diff --git a/.github/workflows/sync-schema.yml b/.github/workflows/sync-schema.yml index d33f307..cfae555 100644 --- a/.github/workflows/sync-schema.yml +++ b/.github/workflows/sync-schema.yml @@ -35,6 +35,9 @@ jobs: - name: Export editor schemas run: wfctl editor-schemas --output src/generated/engine-schemas.json + - name: Export DSL reference + run: wfctl dsl-reference > src/generated/dsl-reference.json + - name: Bump version run: | WORKFLOW_VERSION="${{ github.event.client_payload.version }}" diff --git a/docs/plans/2026-03-26-multifile-config-validation-design.md b/docs/plans/2026-03-26-multifile-config-validation-design.md new file mode 100644 index 0000000..ebdd7fe --- /dev/null +++ b/docs/plans/2026-03-26-multifile-config-validation-design.md @@ -0,0 +1,532 @@ +# Multi-File Config Validation & YAML Side-Pane — Design Document + +**Date:** 2026-03-26 +**Status:** Draft +**Repos:** workflow-editor, workflow-vscode, workflow-jetbrains + +## Overview + +Extend the workflow editor's multi-file config support with comprehensive test validation across config split permutations, an optional YAML side-pane with multi-file navigation, visual file boundaries on the canvas, and node-to-file navigation hooks. The goal is to ensure the visual editor can faithfully represent the entirety of an application configuration regardless of how the YAML files are organized, and to provide a common interface that IDE plugins can hook into for source-level navigation. + +## 1. Problem Statement + +### Current State + +The editor already supports multi-file configs via `resolveImports()` and tracks source provenance in `sourceMap` (module/pipeline name → source file path). Nodes show a file badge on hover when multiple source files exist. `exportToFiles()` can split config back to per-file YAML. + +### Gaps + +1. **Limited test coverage of split permutations** — Only one multi-file fixture set exists (`test-fixtures/multifile/`), covering a single split strategy (application-level imports). Real projects split configs by domain, by architectural layer, and across nested directory trees. We have no test validation for these patterns. + +2. **No YAML side-pane in the editor itself** — When the editor is used standalone (outside an IDE), there is no way to view the YAML alongside the canvas. When embedded in IDE plugins (workflow-vscode, workflow-jetbrains), the IDE's own text editor shows a single merged file, but there is no multi-file tab navigation or file-scoped view. + +3. **No visual file boundaries on canvas** — When a config spans multiple files, there is no visual grouping on the canvas showing which nodes belong to which file. Users have to hover each node to see the file badge. + +4. **Incomplete node-to-file navigation** — `onNavigateToSource` passes `(line, col)` but not the file path. For multi-file configs, clicking a node from `api.yaml` should navigate to the correct line *in that specific file*, not a line in the merged config. + +5. **No reverse navigation** — When a user is viewing YAML and clicks on a section, the corresponding node should be selected and scrolled into view on the canvas. + +## 2. Multi-File Config Split Strategies + +Real applications split configs in at least three distinct patterns. We must test all of them. + +### 2.1 Split by Domain + +Each business domain owns its modules and pipelines in a separate file. A root config imports all domains. + +``` +project/ +├── app.yaml ← root: application metadata + imports +├── domains/ +│ ├── auth/ +│ │ └── auth.yaml ← modules: [auth-db, auth-cache], pipelines: [login, register, verify] +│ ├── billing/ +│ │ └── billing.yaml ← modules: [billing-db, stripe], pipelines: [charge, refund, invoice] +│ └── notifications/ +│ └── notifications.yaml ← modules: [email-svc, sms-svc], pipelines: [send-email, send-sms] +└── shared/ + └── infra.yaml ← modules: [http-server, router, logger] +``` + +**Characteristics:** +- Each domain file has both `modules:` and `pipelines:` +- The root file has `imports:` + `application:` + `workflows:` (routes reference pipelines from domain files) +- Modules in domain files may reference shared infrastructure modules by name + +### 2.2 Split by Architectural Layer + +Config is split horizontally: infrastructure, middleware, business logic, API surface. + +``` +project/ +├── app.yaml ← root: imports + workflows + application +├── layers/ +│ ├── infrastructure.yaml ← modules: [db, cache, message-queue, logger] +│ ├── middleware.yaml ← pipelines: [auth-middleware, rate-limit, cors] +│ ├── services.yaml ← pipelines: [user-service, order-service, product-service] +│ └── api.yaml ← modules: [http-server, router], workflows: {http: ...} +``` + +**Characteristics:** +- Some files have only `modules:`, some only `pipelines:`, some have `workflows:` +- Cross-layer references (services reference infrastructure modules) +- The API layer defines routes that reference service-layer pipelines + +### 2.3 Split by Nested Directories + +Deep nesting with sub-imports. Each level imports its children. + +``` +project/ +├── app.yaml ← imports: [platform/platform.yaml] +├── platform/ +│ ├── platform.yaml ← imports: [core/core.yaml, features/features.yaml] +│ ├── core/ +│ │ ├── core.yaml ← imports: [database.yaml, cache.yaml] +│ │ ├── database.yaml ← modules: [primary-db, replica-db] +│ │ └── cache.yaml ← modules: [redis-cache] +│ └── features/ +│ ├── features.yaml ← imports: [auth.yaml, payments.yaml] +│ ├── auth.yaml ← modules + pipelines for auth +│ └── payments.yaml ← modules + pipelines for payments +``` + +**Characteristics:** +- 3+ levels of nesting +- Intermediate files are pure import aggregators (no modules/pipelines of their own) +- Relative paths in imports are resolved relative to the importing file's directory + +## 3. YAML Side-Pane (Optional, Embeddable) + +### 3.1 Motivation + +IDE plugins (workflow-vscode, workflow-jetbrains) already have their own text editors, so they don't need the editor to render YAML. But: + +1. The **standalone editor** (browser, Storybook, demos) has no YAML view at all +2. We need a **common interface** for multi-file YAML navigation that IDE plugins can either use or replicate +3. Testing the node↔YAML navigation hooks requires an in-editor YAML view + +### 3.2 Design + +Add an optional YAML side-pane to the right side of the editor (collapsible, like PropertyPanel). The pane shows: + +``` +┌──────────────────────────────────────────────────────────┐ +│ [app.yaml ▼] [auth.yaml] [billing.yaml] [infra.yaml] │ ← file tabs +├──────────────────────────────────────────────────────────┤ +│ 1 application: │ +│ 2 name: my-app │ +│ 3 version: 1.0.0 │ +│ 4 │ +│ 5 imports: │ +│ 6 - domains/auth/auth.yaml │ +│ 7 - domains/billing/billing.yaml │ +│ 8 - shared/infra.yaml │ +│ 9 │ +│ 10 workflows: │ +│ 11 http: │ +│ 12 server: http-server │ +│ 13 router: router │ +│ 14 routes: │ +│ 15 >>> - method: POST │ ← highlighted (selected node) +│ 16 >>> path: /api/auth/login │ +│ 17 >>> handler: login │ +│ 18 - method: POST │ +│ 19 path: /api/auth/register │ +│ 20 handler: register │ +└──────────────────────────────────────────────────────────┘ +``` + +**File tabs:** One tab per file in the workspace. Active tab is highlighted. Clicking a tab switches the YAML view to that file's content. + +**Line highlighting:** When a node is selected on the canvas, the YAML pane scrolls to and highlights the corresponding lines in the appropriate file (auto-switching tabs if the node belongs to a different file). + +**Click-to-select:** Clicking on a YAML line in the pane selects the corresponding node on the canvas and scrolls it into view. + +### 3.3 Component Architecture + +```typescript +// New component: src/components/yaml/YamlSidePane.tsx +interface YamlSidePaneProps { + /** Map of file path → YAML content. null key = main file. */ + files: Map; + /** Currently active file tab */ + activeFile: string | null; + /** Called when user switches file tab */ + onFileSelect: (filePath: string | null) => void; + /** Line range to highlight (1-based) */ + highlightRange?: { startLine: number; endLine: number }; + /** Called when user clicks a line */ + onLineClick?: (filePath: string | null, line: number) => void; + /** Whether the pane is visible */ + visible: boolean; +} +``` + +### 3.4 IDE Plugin Integration + +The YAML side-pane is **optional** — controlled by a new prop: + +```typescript +interface WorkflowEditorProps { + // ... existing props ... + /** When true, shows the built-in YAML side-pane. Default: false. + * IDE plugins typically set this to false and use their own text editor. */ + showYamlPane?: boolean; +} +``` + +IDE plugins do NOT use the built-in YAML pane. Instead, they use the **navigation hooks**: + +```typescript +interface WorkflowEditorProps { + // ... existing props ... + /** Enhanced navigation callback. When filePath is provided, navigate to + * the specified line in that file. Backward-compatible: hosts that only + * handle (line, col) can ignore the first argument. + * Overloaded: (line: number, col: number) => void — legacy single-file + * | (filePath: string | null, line: number, col: number) => void — multi-file */ + onNavigateToSource?: (...args: [number, number] | [string | null, number, number]) => void; + /** NEW: Called when the editor wants the host to reveal a specific node. + * The host should select the node on canvas (the editor handles this internally, + * but the host may also want to update its own UI). */ + onNodeFocusRequest?: (nodeId: string) => void; +} +``` + +**Backward compatibility:** The `onNavigateToSource` callback uses a discriminated overload: callers detect the arity or first-argument type to distinguish `(line, col)` from `(filePath, line, col)`. This means existing IDE plugins (workflow-vscode, workflow-jetbrains) continue to work without changes. They can adopt the `filePath` parameter incrementally by checking `typeof args[0] === 'string'` in their bridge handlers. + +## 4. Visual File Boundaries on Canvas + +### 4.1 Concept + +When a multi-file config is loaded, nodes from different source files should be visually grouped. This uses React Flow's built-in **group node** mechanism (which the editor already supports via `GroupNode.tsx`). + +### 4.2 Implementation + +For each unique source file in the workspace, create a background group node: + +```typescript +interface FileGroupNode { + id: `file-group:${string}`; + type: 'fileGroup'; + data: { + label: string; // e.g., "auth.yaml" + filePath: string; // full relative path + fileType: WorkflowFileType; + }; + style: { + backgroundColor: string; // subtle tint per file (auto-assigned from palette) + borderColor: string; + borderStyle: 'dashed'; + borderRadius: 8; + }; + // Position/size computed from child nodes' bounding box +} +``` + +**Behavior:** +- File group nodes are rendered as dashed-border containers behind their child nodes +- Each file gets a distinct subtle background color from a palette (8 colors, cycling) +- The file name is shown in the top-left corner of the group +- Clicking the group label triggers `onNavigateToSource(filePath, 1, 0)` — jump to line 1 of that file +- Group boundaries auto-resize when nodes are moved +- Groups are created only when `sourceMap` has entries from 2+ distinct files + +### 4.3 File Group Color Palette + +```typescript +const FILE_GROUP_COLORS = [ + { bg: '#EFF6FF', border: '#93C5FD' }, // blue + { bg: '#F0FDF4', border: '#86EFAC' }, // green + { bg: '#FFF7ED', border: '#FDBA74' }, // orange + { bg: '#FAF5FF', border: '#C4B5FD' }, // purple + { bg: '#FEF2F2', border: '#FCA5A5' }, // red + { bg: '#ECFEFF', border: '#67E8F9' }, // cyan + { bg: '#FFFBEB', border: '#FCD34D' }, // yellow + { bg: '#FDF2F8', border: '#F9A8D4' }, // pink +]; +``` + +## 5. Enhanced Node-to-File Navigation + +### 5.1 YAML Line Map Enhancement + +The existing `yamlLineMap.ts` only maps module names within a single `modules:` block. Extend it to also map: + +- **Pipeline names** — line range where each pipeline is defined +- **Workflow names** — line range where each workflow section starts +- **Pipeline step names** — line range for individual steps within a pipeline +- **Trigger names** — line range for trigger definitions + +```typescript +export interface YamlLineRange { + startLine: number; + endLine: number; +} + +export interface MultiFileYamlLineMap { + /** file path → { node/section name → line range } */ + files: Map>; +} + +/** + * Build a comprehensive line map across all files in the workspace. + * Covers modules, pipelines, pipeline steps, workflows, and triggers. + */ +export function buildMultiFileLineMap( + files: Map, +): MultiFileYamlLineMap; +``` + +### 5.2 Navigation Flow + +**Canvas → YAML (clicking a node):** + +1. User clicks a node on the canvas +2. Editor looks up `node.data.sourceFile` to determine which file the node belongs to +3. Editor looks up `node.data.label` in the `MultiFileYamlLineMap` for that file +4. If YAML pane is active: switch to the file tab, scroll to and highlight the line range +5. If IDE embedded: call `onNavigateToSource(filePath, startLine, 0)` + +**YAML → Canvas (clicking a YAML line):** + +1. User clicks a line in the YAML pane (or IDE sends a navigate-to-node message) +2. Determine which node the line corresponds to (reverse lookup in `MultiFileYamlLineMap`) +3. Select the node on the canvas +4. Scroll/pan the canvas to center the node +5. Open the property panel for the node + +### 5.3 IDE Bridge Protocol + +For IDE plugins that use the webview bridge, add new message types: + +```typescript +// Editor → Host (node clicked, navigate to source) +interface NavigateToSourceMessage { + type: 'navigateToSource'; + filePath?: string | null; // optional — omitted for single-file configs + line: number; + col: number; + nodeName?: string; +} + +// Host → Editor (user clicked in YAML, navigate to node) +interface NavigateToNodeMessage { + type: 'navigateToNode'; + filePath: string | null; + line: number; +} + +// Host → Editor (file changed externally, reload) +interface FileChangedMessage { + type: 'fileChanged'; + filePath: string | null; + content: string; +} +``` + +## 6. Test Validation Strategy + +### 6.1 Fixture-Based Serialization Tests + +Create three new fixture sets covering each split strategy: + +``` +test-fixtures/ +├── multifile/ ← existing (simple application imports) +├── multifile-domain/ ← NEW: split by domain +│ ├── app.yaml +│ ├── domains/ +│ │ ├── auth.yaml +│ │ ├── billing.yaml +│ │ └── notifications.yaml +│ └── shared/ +│ └── infra.yaml +├── multifile-layers/ ← NEW: split by layer +│ ├── app.yaml +│ ├── layers/ +│ │ ├── infrastructure.yaml +│ │ ├── middleware.yaml +│ │ ├── services.yaml +│ │ └── api.yaml +└── multifile-nested/ ← NEW: deep nesting + ├── app.yaml + └── platform/ + ├── platform.yaml + ├── core/ + │ ├── core.yaml + │ ├── database.yaml + │ └── cache.yaml + └── features/ + ├── features.yaml + ├── auth.yaml + └── payments.yaml +``` + +### 6.2 Test Matrix + +For each fixture set, test: + +| Test | What it validates | +|------|-------------------| +| **resolve-all-modules** | `resolveImports()` finds every module from every file | +| **sourceMap-correctness** | Every module and pipeline gets the correct source file path | +| **round-trip-export** | `exportToFiles()` puts each module/pipeline back in its source file | +| **main-file-imports** | Main file output contains `imports:` references, not inlined content | +| **no-cross-file-bleed** | Modules from file A don't appear in file B's export | +| **no-duplication** | No module or pipeline appears twice after merging | +| **node-creation** | `configToNodes()` creates correct node count with correct labels | +| **sourceFile-on-nodes** | Every node's `data.sourceFile` matches the sourceMap | +| **edge-creation** | Edges connect nodes across file boundaries (e.g., route → pipeline in different file) | +| **pipeline-steps** | Pipeline step nodes are created with correct `pipelineName` | +| **name-version-preserved** | Application name and version survive round-trip | +| **edit-stays-in-file** | Modifying a node and re-exporting keeps it in its original file | +| **cycle-detection** | Circular imports don't cause infinite loops | +| **missing-file-error** | Missing imported files produce errors but don't crash | +| **nested-path-resolution** | 3+ levels of imports resolve relative paths correctly | + +### 6.3 YAML Line Map Tests + +For each fixture, test the `buildMultiFileLineMap`: + +| Test | What it validates | +|------|-------------------| +| **module-lines** | Each module name maps to correct line range in its source file | +| **pipeline-lines** | Each pipeline name maps to correct line range | +| **step-lines** | Each pipeline step maps to correct line range within its pipeline | +| **workflow-lines** | Each workflow section maps to correct line range | +| **cross-file-lookup** | Looking up a node returns the correct file + line | + +### 6.4 Navigation Hook Tests + +Unit tests for the navigation flow: + +| Test | What it validates | +|------|-------------------| +| **node-click-calls-navigate** | Clicking a node with sourceFile calls `onNavigateToSource(filePath, line, col)` | +| **node-click-switches-tab** | When YAML pane is active, clicking a node from a different file switches the tab | +| **yaml-click-selects-node** | Clicking a YAML line selects the corresponding node on canvas | +| **yaml-click-cross-file** | Clicking a line in file B's tab selects a node from file B | +| **no-navigate-without-sourceMap** | Without sourceMap, `onNavigateToSource` passes null filePath | + +### 6.5 Visual Validation Tests (E2E) + +Playwright-based visual tests for the file boundaries and YAML pane: + +| Test | What it validates | +|------|-------------------| +| **file-groups-rendered** | Multi-file config shows dashed group boundaries per file | +| **file-group-labels** | Each group has the correct file name label | +| **file-group-colors** | Groups have distinct background colors | +| **yaml-pane-toggle** | YAML pane shows/hides via toolbar button | +| **yaml-pane-file-tabs** | YAML pane shows tabs for each file | +| **yaml-pane-highlight** | Selecting a node highlights corresponding YAML lines | +| **yaml-pane-tab-switch** | Selecting a node from a different file switches tabs | +| **yaml-click-selects** | Clicking in YAML pane selects node on canvas | +| **file-group-click** | Clicking a file group label triggers navigation | + +### 6.6 Component Tests (Vitest + React Testing Library) + +| Test | What it validates | +|------|-------------------| +| **YamlSidePane-renders** | Component renders file tabs and YAML content | +| **YamlSidePane-tab-switch** | Clicking a tab calls `onFileSelect` | +| **YamlSidePane-highlight** | Line highlight renders at correct position | +| **YamlSidePane-line-click** | Clicking a line calls `onLineClick` | +| **YamlSidePane-hidden** | When `visible=false`, pane is not rendered | +| **FileGroupNode-renders** | File group node renders with correct label and style | +| **FileGroupNode-click** | Clicking group label triggers callback | + +## 7. Additional Editor Functionality + +Beyond the core multi-file and YAML pane features, the following related capabilities should be included in the implementation plan: + +### 7.1 File-Scoped Validation Errors + +When schema validation errors occur, they should be attributed to the correct source file: + +```typescript +interface ValidationError { + nodeId?: string; + message: string; + filePath?: string | null; // NEW: which file the error originates from + line?: number; // NEW: line in the source file +} +``` + +The YAML pane should show inline error markers (red squiggle underline or gutter icon) at the error line. IDE plugins receive the `filePath` and `line` to show errors in their own editors. + +### 7.2 File-Aware Undo/Redo + +Currently undo/redo operates on the merged config. With multi-file awareness: +- Undo/redo should track which file was modified +- The change description should include the file name: "Modified auth.yaml: renamed module 'auth-db' to 'auth-database'" +- The YAML pane should update to show the file that was changed + +### 7.3 Add-Node File Assignment + +When a user adds a new node via the palette, the editor should: +1. If file groups are visible, and the node is dropped inside a file group → assign to that file +2. If dropped outside any group → assign to the main file +3. The PropertyPanel should show a "Source File" field that can be changed via dropdown + +### 7.4 File-Level Export/Import + +Add toolbar actions for file-level operations: +- **"Export File..."** — export a single file's YAML (useful for extracting a domain) +- **"Import File..."** — import YAML into the workspace as a new file (adds an `imports:` entry to the root) +- **"Move to File..."** — right-click a node → move it to a different file (updates sourceMap) + +### 7.5 Workspace Summary Panel + +A small info panel (tooltip or expandable section in the toolbar) showing: +- Total file count +- Module count per file +- Pipeline count per file +- Any unresolved imports or validation errors + +## 8. Implementation Phases + +### Phase 1: Test Fixtures & Serialization Validation +- Create 3 new multi-file fixture sets (domain, layers, nested) +- Write serialization tests for all 3 patterns +- Ensure `resolveImports()` handles nested-directory relative paths +- Fix any bugs discovered by the new test permutations + +### Phase 2: Enhanced YAML Line Map & Navigation Hooks +- Extend `yamlLineMap.ts` to map pipelines, steps, workflows, triggers +- Build `MultiFileYamlLineMap` for cross-file line resolution +- Update `onNavigateToSource` signature to include `filePath` +- Add `onNodeFocusRequest` callback +- Write unit tests for line map and navigation + +### Phase 3: YAML Side-Pane Component +- Implement `YamlSidePane` component with file tabs, syntax coloring, line numbers +- Add `showYamlPane` prop to `WorkflowEditorProps` +- Integrate with `uiLayoutStore` for collapse/resize state +- Wire node selection → YAML highlight (canvas → pane) +- Wire YAML click → node selection (pane → canvas) +- Write component tests + +### Phase 4: Visual File Boundaries +- Implement `FileGroupNode` component +- Auto-generate file group nodes from sourceMap +- Assign colors from palette +- Auto-size groups from child node bounding boxes +- Wire group label click → navigation +- Write component and visual tests + +### Phase 5: IDE Plugin Bridge Updates +- Update webview bridge message protocol in both IDE plugins +- Add `navigateToSource` message with `filePath` field +- Add `navigateToNode` reverse navigation message +- Add `fileChanged` live reload message +- Test with both VSCode and JetBrains plugins + +### Phase 6: Additional Features +- File-scoped validation errors +- Add-node file assignment (drop into file group) +- "Move to File..." context menu +- Workspace summary panel +- File-aware undo/redo descriptions diff --git a/docs/plans/2026-03-26-multifile-config-validation-implementation.md b/docs/plans/2026-03-26-multifile-config-validation-implementation.md new file mode 100644 index 0000000..e678751 --- /dev/null +++ b/docs/plans/2026-03-26-multifile-config-validation-implementation.md @@ -0,0 +1,608 @@ +# Multi-File Config Validation & YAML Side-Pane — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Comprehensive test validation of multi-file workflow configs across domain-split, layer-split, and nested-directory patterns. Enhanced YAML line mapping. Optional YAML side-pane with multi-file navigation. Visual file boundaries on canvas. IDE-compatible navigation hooks. + +**Architecture:** Three new fixture sets validate each split strategy end-to-end through resolveImports → configToNodes → nodesToConfig → exportToFiles round-trip. Extended yamlLineMap covers modules, pipelines, steps, workflows, and triggers for cross-file line resolution. Optional YamlSidePane renders file tabs + syntax coloring + line highlighting. FileGroupNode renders dashed-border containers per source file. + +**Tech Stack:** TypeScript, React, Vitest, Playwright, @xyflow/react, js-yaml + +**Design Doc:** `docs/plans/2026-03-26-multifile-config-validation-design.md` + +--- + +### Task 1: Domain-Split Test Fixtures + +**Files:** +- Create: `test-fixtures/multifile-domain/app.yaml` +- Create: `test-fixtures/multifile-domain/domains/auth.yaml` +- Create: `test-fixtures/multifile-domain/domains/billing.yaml` +- Create: `test-fixtures/multifile-domain/domains/notifications.yaml` +- Create: `test-fixtures/multifile-domain/shared/infra.yaml` + +**Step 1: Create root config** + +`test-fixtures/multifile-domain/app.yaml`: +```yaml +application: + name: my-platform + version: 3.0.0 + +imports: + - domains/auth.yaml + - domains/billing.yaml + - domains/notifications.yaml + - shared/infra.yaml + +workflows: + http: + server: http-server + router: router + routes: + - method: POST + path: /api/auth/login + handler: login + - method: POST + path: /api/auth/register + handler: register + - method: POST + path: /api/billing/charge + handler: charge + - method: POST + path: /api/billing/refund + handler: refund + - method: POST + path: /api/notify/email + handler: send-email +``` + +**Step 2: Create domain files** + +`test-fixtures/multifile-domain/domains/auth.yaml`: +```yaml +modules: + - name: auth-db + type: database.postgres + config: + host: localhost + port: 5432 + database: auth + - name: auth-cache + type: nosql.redis + config: + host: localhost + port: 6379 + +pipelines: + login: + steps: + - name: parse + type: step.request_parse + - name: validate + type: step.validate + - name: authenticate + type: step.auth_validate + - name: respond + type: step.json_response + register: + steps: + - name: parse + type: step.request_parse + - name: validate + type: step.validate + - name: insert + type: step.db_exec + - name: respond + type: step.json_response +``` + +**Step 3: Create all other domain and shared files following the same pattern** + +**Step 4: Commit** + +--- + +### Task 2: Layer-Split Test Fixtures + +**Files:** +- Create: `test-fixtures/multifile-layers/app.yaml` +- Create: `test-fixtures/multifile-layers/layers/infrastructure.yaml` +- Create: `test-fixtures/multifile-layers/layers/middleware.yaml` +- Create: `test-fixtures/multifile-layers/layers/services.yaml` +- Create: `test-fixtures/multifile-layers/layers/api.yaml` + +**Step 1: Create root config** + +`test-fixtures/multifile-layers/app.yaml`: +```yaml +application: + name: layered-app + version: 1.0.0 + +imports: + - layers/infrastructure.yaml + - layers/middleware.yaml + - layers/services.yaml + - layers/api.yaml +``` + +**Step 2: Create layer files** + +- `infrastructure.yaml` — modules: [primary-db, cache, message-queue, logger] +- `middleware.yaml` — pipelines: [auth-middleware, rate-limit, cors] +- `services.yaml` — pipelines: [user-service, order-service, product-service] +- `api.yaml` — modules: [http-server, router], workflows: {http: routes referencing service pipelines} + +**Step 3: Commit** + +--- + +### Task 3: Nested-Directory Test Fixtures + +**Files:** +- Create: `test-fixtures/multifile-nested/app.yaml` +- Create: `test-fixtures/multifile-nested/platform/platform.yaml` +- Create: `test-fixtures/multifile-nested/platform/core/core.yaml` +- Create: `test-fixtures/multifile-nested/platform/core/database.yaml` +- Create: `test-fixtures/multifile-nested/platform/core/cache.yaml` +- Create: `test-fixtures/multifile-nested/platform/features/features.yaml` +- Create: `test-fixtures/multifile-nested/platform/features/auth.yaml` +- Create: `test-fixtures/multifile-nested/platform/features/payments.yaml` + +**Step 1: Create root config with single top-level import** + +`test-fixtures/multifile-nested/app.yaml`: +```yaml +application: + name: nested-platform + version: 2.0.0 + +imports: + - platform/platform.yaml +``` + +**Step 2: Create platform aggregator** + +`test-fixtures/multifile-nested/platform/platform.yaml`: +```yaml +imports: + - core/core.yaml + - features/features.yaml +``` + +**Step 3: Create core aggregator + leaf files** + +- `core.yaml` — imports: [database.yaml, cache.yaml] +- `database.yaml` — modules: [primary-db, replica-db] +- `cache.yaml` — modules: [redis-cache] + +**Step 4: Create features aggregator + leaf files** + +- `features.yaml` — imports: [auth.yaml, payments.yaml] +- `auth.yaml` — modules: [auth-service], pipelines: [login, register] +- `payments.yaml` — modules: [payment-gateway], pipelines: [charge, refund] + +**Step 5: Commit** + +--- + +### Task 4: Domain-Split Serialization Tests + +**Files:** +- Create: `src/utils/serialization-multifile-domain.test.ts` + +**Step 1: Write tests** + +```typescript +describe('domain-split multi-file config', () => { + // Load all fixtures from test-fixtures/multifile-domain/ + // Build resolver from file map + + it('resolves all modules across domain files and shared infra', async () => { + // Expect: auth-db, auth-cache, billing-db, stripe, email-svc, sms-svc, http-server, router, logger + }); + + it('assigns correct sourceFile for every module', async () => { + // auth-db → domains/auth.yaml, http-server → shared/infra.yaml, etc. + }); + + it('tracks all pipelines in sourceMap', async () => { + // login → domains/auth.yaml, charge → domains/billing.yaml, etc. + }); + + it('round-trip export routes modules to correct domain files', async () => { + // exportToFiles() puts each module back in its domain file + }); + + it('round-trip export routes pipelines to correct domain files', async () => { + // exportToFiles() puts each pipeline back in its domain file + }); + + it('main file has imports: but no modules or pipelines', async () => { + // Main file only has application:, imports:, workflows: + }); + + it('workflows stay in main file (routes reference cross-file pipelines)', async () => { + // Workflows always belong to the main file + }); + + it('creates correct node count from merged config', async () => { + // configToNodes() creates nodes for all modules + synthesized pipeline steps + }); + + it('edges connect cross-file nodes (routes to pipeline handlers)', async () => { + // HTTP route edges connect to pipeline nodes from domain files + }); + + it('no module duplication after merge', async () => { + // Set of module names has no duplicates + }); + + it('editing a domain module keeps it in its domain file', async () => { + // Modify auth-db config, re-export, check it stays in domains/auth.yaml + }); + + it('application name and version preserved', async () => { + // config.name === 'my-platform', config.version === '3.0.0' + }); +}); +``` + +**Step 2: Run tests, fix any resolveImports bugs for this pattern** + +**Step 3: Commit** + +--- + +### Task 5: Layer-Split Serialization Tests + +**Files:** +- Create: `src/utils/serialization-multifile-layers.test.ts` + +**Step 1: Write tests** + +```typescript +describe('layer-split multi-file config', () => { + it('resolves modules from infrastructure and api layers', async () => {}); + it('resolves pipelines from middleware and services layers', async () => {}); + it('sourceMap assigns correct layer file for each module/pipeline', async () => {}); + it('round-trip export: modules stay in their layer file', async () => {}); + it('round-trip export: pipelines stay in their layer file', async () => {}); + it('round-trip export: workflows stay in api layer file', async () => {}); + it('main file only has application: and imports:', async () => {}); + it('no cross-layer bleed in exported files', async () => {}); +}); +``` + +**Step 2: Run tests, fix any bugs** + +**Step 3: Commit** + +--- + +### Task 6: Nested-Directory Serialization Tests + +**Files:** +- Create: `src/utils/serialization-multifile-nested.test.ts` + +**Step 1: Write tests** + +```typescript +describe('nested-directory multi-file config', () => { + it('resolves modules across 3+ levels of nesting', async () => { + // primary-db from platform/core/database.yaml, auth-service from platform/features/auth.yaml + }); + + it('sourceMap uses full relative paths from root', async () => { + // primary-db → platform/core/database.yaml (not just database.yaml) + }); + + it('handles intermediate aggregator files with no modules', async () => { + // platform.yaml, core.yaml, features.yaml are pure import aggregators + }); + + it('round-trip export: leaf modules stay in leaf files', async () => {}); + + it('round-trip export: aggregator files only have imports:', async () => { + // platform.yaml export should only contain imports: [core/core.yaml, features/features.yaml] + }); + + it('relative paths in nested imports resolve correctly', async () => { + // core.yaml imports database.yaml (relative to core/ directory) + // This is resolved as platform/core/database.yaml from the root + }); + + it('missing leaf file in nested structure reports error but resolves siblings', async () => { + // Remove payments.yaml from resolver, auth.yaml modules still appear + }); + + it('main file references only top-level import', async () => { + // Main file has imports: [platform/platform.yaml] only + }); +}); +``` + +**Step 2: Run tests — this is the most complex case, likely to surface path resolution bugs** + +**Step 3: Fix `resolveImports()` if nested relative paths aren't handled correctly** + +Currently `resolveImports()` may not resolve paths relative to the importing file's directory. If `core.yaml` (at `platform/core/core.yaml`) imports `database.yaml`, the resolver should receive `platform/core/database.yaml`, not `database.yaml`. Check and fix the path resolution logic. + +**Step 4: Commit** + +--- + +### Task 7: Enhanced YAML Line Map + +**Files:** +- Modify: `src/utils/yamlLineMap.ts` +- Create: `src/utils/yamlLineMap.test.ts` + +**Step 1: Extend `buildYamlLineMap` to cover all section types** + +The current implementation only maps module names within the `modules:` block. Extend it to also map: +- `pipelines:` → pipeline names → `{ startLine, endLine }` for each pipeline +- Pipeline steps → `pipeline:step` keys → `{ startLine, endLine }` for each step +- `workflows:` → workflow names +- `triggers:` → trigger names + +**Step 2: Add `buildMultiFileLineMap` function** + +```typescript +export function buildMultiFileLineMap( + files: Map, +): MultiFileYamlLineMap { + const result: MultiFileYamlLineMap = { files: new Map() }; + for (const [filePath, content] of files) { + result.files.set(filePath, buildYamlLineMap(content)); + } + return result; +} + +export function lookupNodeInLineMap( + lineMap: MultiFileYamlLineMap, + nodeName: string, + sourceFile?: string, +): { filePath: string | null; range: YamlLineRange } | null; +``` + +**Step 3: Write tests for all section types using the fixture files** + +**Step 4: Commit** + +--- + +### Task 8: Update onNavigateToSource Signature + +**Files:** +- Modify: `src/types/editor.ts` — add filePath parameter +- Modify: `src/components/WorkflowEditor.tsx` — pass filePath from node data +- Modify: `src/components/canvas/WorkflowCanvas.tsx` — update onNodeClick handler +- Create: `src/utils/navigation.ts` — navigation helper functions +- Create: `src/utils/navigation.test.ts` + +**Step 1: Update the type** + +```typescript +// In editor.ts +onNavigateToSource?: (filePath: string | null, line: number, col: number) => void; +``` + +**Step 2: Create navigation helpers** + +```typescript +// src/utils/navigation.ts +export function resolveNodeSourceLocation( + node: WorkflowNode, + lineMap: MultiFileYamlLineMap, + sourceMap: Map, +): { filePath: string | null; line: number; col: number } | null; +``` + +**Step 3: Wire into WorkflowCanvas node click handler** + +When a node is clicked and `onNavigateToSource` is provided, call it with the resolved file path + line. + +**Step 4: Write unit tests for navigation helpers** + +**Step 5: Commit** + +--- + +### Task 9: YAML Side-Pane Component + +**Files:** +- Create: `src/components/yaml/YamlSidePane.tsx` +- Create: `src/components/yaml/YamlSidePane.test.tsx` +- Create: `src/components/yaml/FileTabBar.tsx` +- Create: `src/components/yaml/YamlLineRenderer.tsx` +- Modify: `src/types/editor.ts` — add `showYamlPane` prop +- Modify: `src/components/WorkflowEditor.tsx` — integrate YamlSidePane +- Modify: `src/stores/uiLayoutStore.ts` — add yamlPane collapse/width state + +**Step 1: Create FileTabBar component** + +Simple tab bar showing one tab per file. Active tab highlighted. Click to switch. + +```tsx +interface FileTabBarProps { + files: Array<{ path: string | null; label: string }>; + activeFile: string | null; + onSelect: (filePath: string | null) => void; +} +``` + +**Step 2: Create YamlLineRenderer component** + +Renders YAML content with line numbers, syntax coloring (simple keyword highlighting for YAML keys, strings, comments), and line highlight range. + +**Step 3: Create YamlSidePane component** + +Combines FileTabBar + YamlLineRenderer. Handles scroll-to-line behavior. + +**Step 4: Add `showYamlPane` prop to WorkflowEditorProps** + +**Step 5: Integrate into WorkflowEditor layout** + +When `showYamlPane` is true, add a fourth panel to the right of the property panel (or replace the property panel with a split view). + +Layout: +``` +┌─────────┬───────────────────┬────────────┬──────────────┐ +│ Palette │ Canvas │ Properties │ YAML Pane │ +│ │ │ │ (optional) │ +└─────────┴───────────────────┴────────────┴──────────────┘ +``` + +**Step 6: Wire node selection → YAML highlight** + +When `selectedNodeId` changes in workflowStore, compute the line range and active file, update YamlSidePane props. + +**Step 7: Wire YAML click → node selection** + +When `onLineClick` fires, reverse-lookup the node from the line map, call `setSelectedNodeId()`. + +**Step 8: Write component tests** + +```typescript +describe('YamlSidePane', () => { + it('renders file tabs for each file in the map'); + it('switches content when tab is clicked'); + it('highlights lines in the specified range'); + it('scrolls to highlighted lines'); + it('calls onLineClick when a line is clicked'); + it('does not render when visible=false'); +}); + +describe('FileTabBar', () => { + it('renders one tab per file'); + it('marks active tab with active class'); + it('calls onSelect with file path when tab is clicked'); + it('shows "main" for null file path'); +}); +``` + +**Step 9: Commit** + +--- + +### Task 10: Visual File Boundary Groups + +**Files:** +- Create: `src/components/nodes/FileGroupNode.tsx` +- Create: `src/components/nodes/FileGroupNode.test.tsx` +- Modify: `src/utils/serialization.ts` — add file group generation +- Modify: `src/stores/nodeTypeRegistry.ts` — register FileGroupNode +- Create: `src/utils/fileGroups.ts` — file group computation utilities +- Create: `src/utils/fileGroups.test.ts` + +**Step 1: Create FileGroupNode component** + +A React Flow group node with dashed border, subtle background color, and a file name label. + +**Step 2: Create file group computation** + +```typescript +// src/utils/fileGroups.ts +export function computeFileGroups( + nodes: WorkflowNode[], + sourceMap: Map, +): FileGroupData[]; + +export interface FileGroupData { + filePath: string; + nodeIds: string[]; + bounds: { x: number; y: number; width: number; height: number }; + color: { bg: string; border: string }; +} +``` + +**Step 3: Integrate into configToNodes or as a post-processing step** + +After `configToNodes()` creates all nodes, call `computeFileGroups()` to generate group nodes. Insert them into the nodes array with `type: 'fileGroup'`. + +**Step 4: Register FileGroupNode in node type registry** + +**Step 5: Write tests** + +```typescript +describe('computeFileGroups', () => { + it('creates one group per unique sourceFile'); + it('does not create groups when only one source file'); + it('assigns distinct colors to each group'); + it('computes bounds from child node positions'); + it('handles nodes with no sourceFile (main file group)'); +}); +``` + +**Step 6: Commit** + +--- + +### Task 11: E2E Visual Validation Tests + +**Files:** +- Modify: `e2e/editor.spec.ts` — add multi-file visual tests + +**Step 1: Create a test that loads a multi-file config and checks visual rendering** + +```typescript +test('multi-file config shows file group boundaries', async ({ page }) => { + // Load domain-split config + // Check for file group nodes with dashed borders + // Check for distinct background colors + // Check for file name labels +}); + +test('clicking a node highlights YAML in side-pane', async ({ page }) => { + // Enable showYamlPane + // Load multi-file config + // Click a node + // Check YAML pane shows correct file tab and highlighted lines +}); + +test('clicking YAML line selects node on canvas', async ({ page }) => { + // Enable showYamlPane + // Load multi-file config + // Click a line in the YAML pane + // Check the corresponding node is selected on canvas +}); +``` + +**Step 2: Commit** + +--- + +### Task 12: IDE Plugin Bridge Updates (Design Only) + +**NOTE:** This task describes changes needed in workflow-vscode and workflow-jetbrains repos. The implementation is tracked separately. + +**workflow-vscode changes:** +- Update webview bridge to handle `navigateToSource` with `filePath` parameter +- When filePath is non-null, open the file in a text editor tab and go to the line +- Send `navigateToNode` messages when user clicks in YAML in the IDE text editor +- Listen for `fileChanged` messages to hot-reload when files are edited externally + +**workflow-jetbrains changes:** +- Update JCEF bridge message handler for `navigateToSource` with `filePath` +- Use `FileEditorManager.openFile()` to navigate to the correct file + line +- Send `navigateToNode` messages from `CaretListener` when cursor moves in YAML +- File watcher already exists; extend to send `fileChanged` to webview + +**Commit:** N/A (tracked in other repos) + +--- + +## Summary of Deliverables + +| Task | Type | Files | +|------|------|-------| +| 1-3 | Test fixtures | 18 new YAML files across 3 fixture sets (domain=5, layers=5, nested=8) | +| 4-6 | Serialization tests | 3 new test files (~30 test cases each) | +| 7 | YAML line map | Extended yamlLineMap.ts + new test file | +| 8 | Navigation hooks | Updated types + new navigation utils | +| 9 | YAML side-pane | 3 new components + component tests | +| 10 | File boundaries | New FileGroupNode + fileGroups utils | +| 11 | E2E tests | Extended Playwright tests | +| 12 | IDE bridge design | Documentation only (impl in other repos) | diff --git a/docs/plans/2026-03-27-editor-completeness-design.md b/docs/plans/2026-03-27-editor-completeness-design.md new file mode 100644 index 0000000..b1c67b4 --- /dev/null +++ b/docs/plans/2026-03-27-editor-completeness-design.md @@ -0,0 +1,220 @@ +# Editor Completeness: Schema-Driven Testing, Typed Forms, DSL Reference & Navigation + +**Date:** 2026-03-27 +**Status:** Approved +**Repos:** workflow-editor, workflow, workflow-vscode, workflow-jetbrains + +## Overview + +Five interconnected workstreams to ensure every aspect of the workflow YAML DSL renders correctly in the visual editor, every node's attributes are visible and editable with proper form widgets, and users can understand and navigate the DSL whether they're in the visual editor, an IDE, or writing raw YAML. + +## 1. Schema-Driven Test Matrix + +Auto-generate rendering and property panel tests for every module type from `engine-schemas.json`. New types added via schema sync automatically get test coverage. + +### Architecture + +```mermaid +graph LR + A[engine-schemas.json] -->|read at test time| B[test matrix generator] + B --> C[per-type rendering test] + B --> D[per-type property panel test] + C --> E[correct node component] + C --> F[correct icon/color/category] + D --> G[all ConfigFieldDef fields visible] + D --> H[correct widget per field type] + D --> I[field editable + value roundtrips] +``` + +### Implementation + +- `src/utils/schema-matrix.test.ts` — reads `engine-schemas.json`, iterates every module type +- For each type: creates a minimal config → `configToNodes()` → asserts correct `nodeComponentType` mapping → asserts node data has correct category color +- For each type with `configFields`: mounts `PropertyPanel` with a selected node of that type → asserts every field renders with the correct widget type → asserts editing a field calls `updateNodeConfig` +- `describe.each()` pattern so vitest shows one named test per module type +- When schema sync adds a new type, the test matrix auto-expands + +### Coverage + +- Pipeline step nodes (synthesized) — verify they render as `integrationNode` +- Conditional nodes — verify diamond shape, handle layout per subtype +- Partial YAML — verify modules-only, pipelines-only, imports-only all render without errors + +## 2. Typed Schema Generation (Workflow Engine) + +Eliminate `type: "json"` catch-all fields by generating proper typed schemas from Go structs. This becomes the authoritative schema source and represents a major version bump. + +### Architecture + +```mermaid +graph TB + A[Go module factory] -->|reflect/struct tags| B[schema generator] + B --> C[ConfigFieldDef per field] + C --> D[wfctl editor-schemas output] + D -->|sync-schema CI| E[engine-schemas.json in workflow-editor] + E --> F[PropertyPanel renders typed forms] + E --> G[test matrix validates coverage] +``` + +### Workflow Engine Changes + +- Each module's config struct already has `json`/`yaml` tags. Add struct tags for editor metadata: `editor:"type=select,options=postgres|mysql|sqlite"`, `editor:"description=Database connection string"`, `editor:"required"` +- New `pkg/schema/` package that reflects on config structs → produces `[]ConfigFieldDef` +- `wfctl editor-schemas` enhanced to include full typed fields instead of bare type names +- Every module factory registers its config struct type so the generator can iterate all + +### Validation Contract + +- CI test in workflow repo: for every registered module type, assert `editor-schemas` produces a non-empty `configFields` array with zero `type: "json"` fields +- Any new module that ships with a `json`-typed field fails CI +- Go type safety flows all the way through to the visual editor + +### Migration Path + +1. Add schema generator + struct tags to workflow engine (one module at a time) +2. When all ~70 types have proper schemas → major version bump +3. workflow-editor removes the static `MODULE_TYPES` fallback array — engine schema is authoritative +4. The test matrix catches any regressions + +## 3. DSL Documentation + Reference System + +Make the DSL spec understandable everywhere — in the visual editor, in IDE YAML editing, and in standalone docs. + +### DSL Hierarchy + +```mermaid +graph TD + A[application] --> B[modules] + A --> C[workflows] + A --> D[pipelines] + A --> E[triggers] + A --> F[imports] + C --> G["http: server + router + routes"] + C --> H["messaging: broker + subscriptions"] + C --> I["statemachine: engine + definitions"] + C --> J["event: processor + handlers"] + G -->|route.handler references| B + B -->|dependsOn references| B + D -->|steps array| K["step.* types"] + E -->|fires| D + F -->|merges from files| B + F -->|merges from files| D +``` + +### Layer 1: Canonical DSL Reference (workflow repo) + +- `docs/dsl-reference.md` — the authoritative spec +- Sections: application, modules, workflows (http/messaging/statemachine/event), pipelines, triggers, imports, config providers, sidecars, platform, infrastructure +- Each section: purpose, required fields, optional fields, relationship to other sections, minimal example +- Machine-parseable frontmatter per section so consumers can extract structured data +- Generated from engine source where possible (module type list, step type list, trigger type list) +- `wfctl docs` command to render the reference locally + +### Layer 2: Editor DSL Reference Pane (workflow-editor) + +- Collapsible sidebar pane (like PropertyPanel) with a book icon in toolbar +- Content loaded from bundled `dsl-reference.json` (extracted from markdown at build time, synced alongside engine-schemas.json) +- **Context-sensitive:** when a node is selected, pane auto-scrolls to the relevant section +- **Section hierarchy:** mirrors the YAML structure — click through application → modules → specific type +- Includes inline YAML examples that match the visual canvas representation +- Shares the right panel area as a tab alongside YAML pane when both are active + +### Layer 3: IDE Plugin Integration (workflow-vscode + workflow-jetbrains) + +- **Hover tooltips:** cursor on a YAML key (`modules:`, `workflows:`, `pipelines:`) shows the DSL reference description +- **Autocomplete descriptions:** YAML language server suggestions include DSL reference detail +- **Command palette:** `Workflow: Show DSL Reference` opens the reference as a webview or markdown preview +- Both plugins already have the webview bridge — the reference pane from Layer 2 works inside the IDE webview + +## 4. Navigation — Breadcrumbs + Interactive File Groups + +### Breadcrumb Bar + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 📁 app.yaml › domains/ › auth.yaml › login pipeline │ +└──────────────────────────────────────────────────────────────┘ +``` + +- Renders above the canvas, below the toolbar +- Shows current file context path based on selected node's `sourceFile` +- Each segment clickable: root config, directories, files, pipeline names +- When no node selected, shows just the root config path +- In IDE mode: clicks call `onNavigateToSource(filePath, 1, 0)` to open in IDE + +### Interactive File Groups + +Enhance existing `FileGroupNode` (currently `pointer-events: none`): + +- **Group header clickable:** clicking the filename label navigates to that file +- **Group border clickable:** clicking the border pans + zooms canvas to fit that file's nodes +- **Double-click group:** opens file in YAML pane or triggers IDE navigation +- **Visual affordance:** pointer cursor on hover, subtle highlight on border +- Keep `pointer-events: none` on group background so contained nodes remain clickable + +### Cross-File Node Interaction + +When clicking a node in a different file group: +1. Selects the node +2. Updates breadcrumb to reflect new file context +3. YAML pane switches to that file's tab and highlights node's lines +4. IDE mode calls `onNavigateToSource(filePath, line, col)` + +### Partial Config "Navigate to Parent" + +- Breadcrumb shows `? › domains/ › auth.yaml` when parent unknown +- If root config resolved via `discoverConfigRoot`, shows `app.yaml › domains/ › auth.yaml` +- Clicking root in breadcrumb: standalone calls `onNavigateToSource`, IDE opens the file + loads merged view +- "View full config" button in toolbar when partial loaded + +## 5. Property Panel Completeness Testing + +Assert every node type's attributes are visible and editable when selected. + +### Test Structure (per module type) + +1. **All fields rendered:** visible field editor count === `configFields.length` in schema +2. **Correct widget type:** string→text input, number→number input, boolean→checkbox, select→dropdown with correct options, array→ArrayFieldEditor, map→MapFieldEditor, json→textarea (flagged as tech debt), sql→SqlEditor, sensitive→password input, filepath→FilePicker +3. **Field metadata:** label matches `field.label`, description/placeholder shown, required indicator when `field.required` +4. **Editing roundtrips:** change value → `updateNodeConfig` called with correct key/value → node config reflects change +5. **Inheritance rendering:** fields with `inheritFrom` show inherited value in italic green +6. **Special editors:** ConditionalCasesEditor for `conditional.switch`, MiddlewareChainEditor for `http.router`, HandlerRoutesEditor for `api.query`/`api.command` + +### Coverage Contract + +- Module type with zero `configFields` → assert just name + type badge + delete (no config section) +- Module type with `json`-typed field → test passes but logs warning: `TECH DEBT: ${type}.${field} uses json textarea` +- CI can fail on `json` fields once engine migration complete — controlled by `STRICT_SCHEMA=true` env var + +## Implementation Phases + +### Phase 1: Schema-Driven Tests (workflow-editor) +- Schema matrix test generator +- Property panel completeness tests +- Partial config rendering tests +- All driven from engine-schemas.json + +### Phase 2: Navigation UX (workflow-editor) +- Breadcrumb bar component +- Interactive file groups (enhance FileGroupNode) +- Cross-file node click → breadcrumb + YAML pane sync +- Partial config "navigate to parent" + "View full config" button + +### Phase 3: DSL Reference (workflow + workflow-editor) +- DSL reference markdown in workflow repo +- `dsl-reference.json` build/sync pipeline +- DSL Reference pane component in workflow-editor +- Context-sensitive section linking + +### Phase 4: Typed Schema Generation (workflow engine) +- `pkg/schema/` generator from Go struct tags +- Editor struct tags on module configs (incremental per module) +- `wfctl editor-schemas` enhancement +- CI contract: no `json`-typed fields +- Major version bump when complete + +### Phase 5: IDE Integration (workflow-vscode + workflow-jetbrains) +- Breadcrumb click → file navigation +- DSL reference hover tooltips +- Autocomplete with DSL descriptions +- `Workflow: Show DSL Reference` command diff --git a/docs/plans/2026-03-27-editor-completeness-implementation.md b/docs/plans/2026-03-27-editor-completeness-implementation.md new file mode 100644 index 0000000..46ee0ec --- /dev/null +++ b/docs/plans/2026-03-27-editor-completeness-implementation.md @@ -0,0 +1,1018 @@ +# Editor Completeness Implementation Plan (Phases 1-2) + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Schema-driven test matrix covering all 279 module types, property panel completeness tests, partial config rendering tests, breadcrumb navigation, and interactive file groups. + +**Architecture:** Auto-generate vitest tests from engine-schemas.json using `describe.each()` to cover every module type's rendering and property panel. Add BreadcrumbBar component for file hierarchy navigation. Enhance existing FileGroupNode to be interactive. All work in workflow-editor on branch `copilot/extend-usability-test-validation`. + +**Tech Stack:** TypeScript, React, Vitest, @testing-library/react, @xyflow/react, Playwright + +**Design Doc:** `docs/plans/2026-03-27-editor-completeness-design.md` + +--- + +### Task 1: Schema-Driven Node Rendering Test Matrix + +**Files:** +- Create: `src/utils/schema-rendering-matrix.test.ts` +- Reference: `src/generated/engine-schemas.json`, `src/utils/serialization.ts` + +**Step 1: Write the test file** + +This test auto-generates a rendering test for every module type in engine-schemas.json. It verifies that `configToNodes()` produces a node with the correct component type and category. + +```typescript +import { describe, it, expect } from 'vitest'; +import engineData from '../generated/engine-schemas.json'; +import { configToNodes, nodeComponentType } from './serialization.ts'; +import { getEngineModuleTypes } from '../generated/load-schemas.ts'; +import type { WorkflowConfig } from '../types/workflow.ts'; + +const moduleTypeMap = getEngineModuleTypes(); +const allTypes = Object.keys((engineData as any).moduleSchemas); + +// Category → expected node component type mapping +const CATEGORY_NODE_MAP: Record = { + http: 'httpNode', // only http.server + messaging: 'messagingNode', + statemachine: 'stateMachineNode', + events: 'eventNode', + scheduling: 'schedulerNode', + middleware: 'middlewareNode', + database: 'databaseNode', + observability: 'observabilityNode', + security: 'securityNode', + integration: 'integrationNode', + infrastructure: 'infrastructureNode', + pipeline: 'integrationNode', + cicd: 'integrationNode', + deployment: 'integrationNode', + platform: 'infrastructureNode', +}; + +describe('schema-driven node rendering matrix', () => { + it(`covers all ${allTypes.length} module types from engine-schemas.json`, () => { + expect(allTypes.length).toBeGreaterThan(0); + }); + + describe.each(allTypes)('module type: %s', (moduleType) => { + it('produces a node via configToNodes', () => { + const config: WorkflowConfig = { + modules: [{ name: 'test-node', type: moduleType, config: {} }], + workflows: {}, + triggers: {}, + }; + const { nodes } = configToNodes(config, moduleTypeMap); + expect(nodes.length).toBe(1); + expect(nodes[0].data.label).toBe('test-node'); + expect(nodes[0].data.moduleType).toBe(moduleType); + }); + + it('maps to a valid node component type', () => { + const componentType = nodeComponentType(moduleType); + const validTypes = [ + 'httpNode', 'httpRouterNode', 'messagingNode', 'stateMachineNode', + 'schedulerNode', 'eventNode', 'integrationNode', 'middlewareNode', + 'infrastructureNode', 'databaseNode', 'securityNode', 'observabilityNode', + 'conditionalNode', + ]; + expect(validTypes).toContain(componentType); + }); + + it('has a category in the schema', () => { + const schema = (engineData as any).moduleSchemas[moduleType]; + expect(schema.category).toBeTruthy(); + }); + }); +}); +``` + +**Step 2: Run tests to verify they pass** + +Run: `npx vitest run src/utils/schema-rendering-matrix.test.ts` +Expected: All 279 × 3 = 837 test cases pass (or close — some types may have quirks to fix). + +**Step 3: Fix any failing tests** + +If certain module types fail `configToNodes` (e.g., conditional types need special config), add exception handling in the test or fix the serialization code. + +**Step 4: Commit** + +```bash +git add src/utils/schema-rendering-matrix.test.ts +git commit -m "test: schema-driven rendering matrix for all 279 module types" +``` + +--- + +### Task 2: Property Panel Schema Completeness Matrix + +**Files:** +- Modify: `src/components/properties/PropertyPanel.schema.test.ts` (expand from 10 types to all 279) + +**Step 1: Refactor the existing schema test to iterate all types** + +Replace the hardcoded `typesToAudit` array with `Object.keys(engineData.moduleSchemas)`. The existing test structure (field count, types, required flags, defaults, options) is already correct — just expand the scope. + +```typescript +// Replace: +// const typesToAudit = ['http.server', 'http.middleware.cors', ...]; +// With: +const typesToAudit = Object.keys((engineData as any).moduleSchemas); +``` + +Keep the existing per-type assertions: +- Field count match (engine vs editor) +- Field types match (with duration→string mapping) +- Required flags match +- Default values match +- Select options match + +**Step 2: Run tests** + +Run: `npx vitest run src/components/properties/PropertyPanel.schema.test.ts` +Expected: Most pass. Some may fail if static MODULE_TYPES is missing types that engine-schemas.json has. + +**Step 3: Fix failures** + +If types exist in engine-schemas.json but not in the editor's moduleTypeMap, they still load via `getEngineModuleTypes()` which reads engine-schemas.json directly. The test should pass because moduleTypeMap includes engine types. If not, investigate the merging logic. + +**Step 4: Commit** + +```bash +git add src/components/properties/PropertyPanel.schema.test.ts +git commit -m "test: expand property panel schema fidelity to all 279 module types" +``` + +--- + +### Task 3: Property Panel Rendering Tests (Component Tests) + +**Files:** +- Create: `src/components/properties/PropertyPanel.rendering.test.tsx` + +**Step 1: Write component rendering tests** + +For a representative set of module types (one per field type combination), mount PropertyPanel with a selected node and verify the correct widgets render. + +```typescript +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, fireEvent, within } from '@testing-library/react'; +import { act } from '@testing-library/react'; +import PropertyPanel from './PropertyPanel.tsx'; +import useWorkflowStore from '../../stores/workflowStore.ts'; +import engineData from '../../generated/engine-schemas.json'; +import { getEngineModuleTypes } from '../../generated/load-schemas.ts'; + +const engineSchemas = (engineData as any).moduleSchemas; +const moduleTypeMap = getEngineModuleTypes(); + +function resetStore() { + useWorkflowStore.setState({ + nodes: [], + edges: [], + selectedNodeId: null, + nodeCounter: 0, + undoStack: [], + redoStack: [], + toasts: [], + showAIPanel: false, + showComponentBrowser: false, + }); +} + +function selectNodeOfType(moduleType: string) { + act(() => { + useWorkflowStore.getState().addNode(moduleType, { x: 0, y: 0 }); + }); + const nodeId = useWorkflowStore.getState().nodes[0].id; + act(() => { + useWorkflowStore.getState().setSelectedNode(nodeId); + }); + return nodeId; +} + +// Widget type expectations per ConfigFieldDef.type +const FIELD_TYPE_WIDGET: Record = { + string: 'textbox', // role="textbox" or input[type="text"] + number: 'spinbutton', // input[type="number"] has role spinbutton + boolean: 'checkbox', // input[type="checkbox"] + select: 'combobox', //