diff --git a/.github/workflows/ci-sync.yml b/.github/workflows/ci-sync.yml
index 06e2bf5a5..bea7a04f8 100644
--- a/.github/workflows/ci-sync.yml
+++ b/.github/workflows/ci-sync.yml
@@ -1,21 +1,14 @@
-name: CI - Surface Sync
+name: CI - Cross-Repo Sync
on:
pull_request:
branches:
- main
- development
- paths:
- - 'src/frontend/**'
- - 'src/middleware/shared/**'
- - 'src/backend/shared/**'
- - 'src/__architecture__/**'
- - 'scripts/compare-surfaces.py'
- - '.github/workflows/ci-sync.yml'
jobs:
surface-sync:
- name: Shared Surface Sync Check
+ name: Shared Surface Sync
runs-on: ubuntu-latest
steps:
@@ -37,8 +30,22 @@ jobs:
with:
path: editor
- - name: Checkout web repo (target branch)
+ - name: Check for shared surface changes
if: steps.token-check.outputs.skip == 'false'
+ id: filter
+ run: |
+ cd editor
+ git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1
+ CHANGED=$(git diff --name-only FETCH_HEAD HEAD)
+ if echo "$CHANGED" | grep -qE '^(src/frontend/|src/middleware/shared/|src/backend/shared/|src/__architecture__/)'; then
+ echo "shared=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "shared=false" >> "$GITHUB_OUTPUT"
+ echo "No shared surface changes detected. Skipping sync check."
+ fi
+
+ - name: Checkout web repo (target branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true'
uses: actions/checkout@v4
with:
repository: Autonomy-Logic/openplc-web
@@ -49,7 +56,7 @@ jobs:
id: checkout-web-target
- name: Checkout web repo (main fallback)
- if: steps.token-check.outputs.skip == 'false' && steps.checkout-web-target.outcome == 'failure'
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.checkout-web-target.outcome == 'failure'
uses: actions/checkout@v4
with:
repository: Autonomy-Logic/openplc-web
@@ -58,12 +65,12 @@ jobs:
token: ${{ secrets.CROSS_REPO_TOKEN }}
- name: Warn about branch fallback
- if: steps.token-check.outputs.skip == 'false' && steps.checkout-web-target.outcome == 'failure'
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.checkout-web-target.outcome == 'failure'
run: |
echo "::warning::Web repo does not have branch '${{ github.event.pull_request.base.ref }}'. Fell back to 'main'."
- name: Compare surfaces against target branch
- if: steps.token-check.outputs.skip == 'false'
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true'
id: compare-target
run: |
set +e
@@ -91,7 +98,7 @@ jobs:
fi
- name: Find matching web PRs
- if: steps.token-check.outputs.skip == 'false' && steps.compare-target.outputs.match == 'False'
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.compare-target.outputs.match == 'False'
id: find-prs
env:
GH_TOKEN: ${{ secrets.CROSS_REPO_TOKEN }}
@@ -118,7 +125,7 @@ jobs:
fi
- name: Check web PRs for sync
- if: steps.token-check.outputs.skip == 'false' && steps.compare-target.outputs.match == 'False' && steps.find-prs.outputs.pr_count != '0'
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.compare-target.outputs.match == 'False' && steps.find-prs.outputs.pr_count != '0'
id: check-prs
run: |
PR_JSON=$(cat /tmp/web-prs.json)
@@ -164,7 +171,7 @@ jobs:
done
- name: Report result
- if: always() && steps.token-check.outputs.skip == 'false'
+ if: always() && steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true'
run: |
MATCH="${{ steps.compare-target.outputs.match }}"
MATCHED_PR="${{ steps.check-prs.outputs.matched_pr }}"
@@ -192,3 +199,219 @@ jobs:
"
exit 1
fi
+
+ deps-sync:
+ name: Shared Dependencies Sync
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check for cross-repo token
+ id: token-check
+ run: |
+ if [ -z "$TOKEN" ]; then
+ echo "::warning::CROSS_REPO_TOKEN is not set. Skipping sync check (expected on fork PRs)."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ fi
+ env:
+ TOKEN: ${{ secrets.CROSS_REPO_TOKEN }}
+
+ - name: Checkout editor repo
+ if: steps.token-check.outputs.skip == 'false'
+ uses: actions/checkout@v4
+ with:
+ path: editor
+
+ - name: Check for package.json changes
+ if: steps.token-check.outputs.skip == 'false'
+ id: filter
+ run: |
+ cd editor
+ git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1
+ CHANGED=$(git diff --name-only FETCH_HEAD HEAD)
+ if echo "$CHANGED" | grep -qE '^package\.json$'; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ echo "No package.json changes detected. Skipping dependency sync check."
+ fi
+
+ - name: Checkout web repo (head branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: ${{ github.head_ref }}
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+ continue-on-error: true
+ id: checkout-web-head
+
+ - name: Checkout web repo (base branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: ${{ github.event.pull_request.base.ref }}
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+ continue-on-error: true
+ id: checkout-web-base
+
+ - name: Checkout web repo (main fallback)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' && steps.checkout-web-base.outcome == 'failure'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: main
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+
+ - name: Compare shared dependencies
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true'
+ run: python3 editor/scripts/compare-dependencies.py --web-root web --editor-root editor
+
+ script-sync:
+ name: Comparison Script Sync
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check for cross-repo token
+ id: token-check
+ run: |
+ if [ -z "$TOKEN" ]; then
+ echo "::warning::CROSS_REPO_TOKEN is not set. Skipping sync check (expected on fork PRs)."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ fi
+ env:
+ TOKEN: ${{ secrets.CROSS_REPO_TOKEN }}
+
+ - name: Checkout editor repo
+ if: steps.token-check.outputs.skip == 'false'
+ uses: actions/checkout@v4
+ with:
+ path: editor
+
+ - name: Check for compare script changes
+ if: steps.token-check.outputs.skip == 'false'
+ id: filter
+ run: |
+ cd editor
+ git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1
+ CHANGED=$(git diff --name-only FETCH_HEAD HEAD)
+ if echo "$CHANGED" | grep -qE '^scripts/compare-surfaces\.py$'; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ echo "No compare script changes detected. Skipping script sync check."
+ fi
+
+ - name: Checkout web repo (head branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: ${{ github.head_ref }}
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+ continue-on-error: true
+ id: checkout-web-head
+
+ - name: Checkout web repo (base branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: ${{ github.event.pull_request.base.ref }}
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+ continue-on-error: true
+ id: checkout-web-base
+
+ - name: Checkout web repo (main fallback)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' && steps.checkout-web-base.outcome == 'failure'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: main
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+
+ - name: Compare SURFACES definitions
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true'
+ run: python3 editor/scripts/compare-surface-definitions.py --web-root web --editor-root editor
+
+ tooling-sync:
+ name: Tooling Configuration Sync
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check for cross-repo token
+ id: token-check
+ run: |
+ if [ -z "$TOKEN" ]; then
+ echo "::warning::CROSS_REPO_TOKEN is not set. Skipping sync check (expected on fork PRs)."
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ fi
+ env:
+ TOKEN: ${{ secrets.CROSS_REPO_TOKEN }}
+
+ - name: Checkout editor repo
+ if: steps.token-check.outputs.skip == 'false'
+ uses: actions/checkout@v4
+ with:
+ path: editor
+
+ - name: Check for tooling changes
+ if: steps.token-check.outputs.skip == 'false'
+ id: filter
+ run: |
+ cd editor
+ git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1
+ CHANGED=$(git diff --name-only FETCH_HEAD HEAD)
+ if echo "$CHANGED" | grep -qE '^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/)'; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ echo "No tooling changes detected. Skipping tooling sync check."
+ fi
+
+ - name: Checkout web repo (head branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: ${{ github.head_ref }}
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+ continue-on-error: true
+ id: checkout-web-head
+
+ - name: Checkout web repo (base branch)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: ${{ github.event.pull_request.base.ref }}
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+ continue-on-error: true
+ id: checkout-web-base
+
+ - name: Checkout web repo (main fallback)
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' && steps.checkout-web-base.outcome == 'failure'
+ uses: actions/checkout@v4
+ with:
+ repository: Autonomy-Logic/openplc-web
+ ref: main
+ path: web
+ token: ${{ secrets.CROSS_REPO_TOKEN }}
+
+ - name: Compare tooling configuration
+ if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true'
+ run: python3 editor/scripts/compare-tooling.py --web-root web --editor-root editor --github-annotations
diff --git a/CLAUDE.md b/CLAUDE.md
index 9dc815965..4c65cb6b9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,130 +1,296 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This file provides guidance to Claude Code when working with the OpenPLC Editor codebase.
+
+## Project Overview
+
+OpenPLC Editor is an **Electron + React** desktop IDE for programming PLCs using IEC 61131-3 languages (Structured Text, Ladder Diagram, Function Block Diagram, Instruction List) plus Python and C++ extensions.
## Build & Development Commands
+**Package manager:** `npm` (not pnpm)
+
```bash
-# Install dependencies
-npm install
+npm install # Install deps + download binaries + build DLL cache
+npm run dev # Full dev mode (main + renderer + Electron, port 1313)
+npm run build # Production build (main + renderer)
+npm run build:main # Electron main process only
+npm run build:renderer # React renderer only
+npm run build:dll # Webpack DLL cache for faster dev rebuilds
+npm run package # Build + create distributable (electron-builder)
+
+npm run lint # ESLint check
+npm run lint:fix # Auto-fix lint issues
+npm run format # Prettier formatting
+
+npm run test # Jest with coverage (enforced thresholds)
+npm run test:watch # Jest watch mode (no coverage)
+npm run test:e2e # Playwright E2E tests
+
+npm run validate:arch # Architecture layer dependency validation
+```
+
+## Architecture
+
+### Layer Overview
+
+```
+src/
+├── main/ # Electron main process (Node.js)
+├── frontend/ # React UI layer (renderer process)
+│ ├── components/ # Atomic Design: _atoms, _molecules, _organisms, _features, _templates
+│ ├── store/ # Zustand store (19 slices)
+│ ├── hooks/ # Custom React hooks
+│ ├── services/ # Business logic and side effects
+│ ├── utils/ # Domain utilities (PLC, graphical, debug, formatters)
+│ ├── data/ # Static data (function libraries, block definitions)
+│ ├── locales/ # i18next translations
+│ └── assets/ # Images, icons
+├── backend/
+│ ├── editor/ # Main process modules (compiler, hardware, modbus, websocket, services)
+│ └── shared/ # Platform-agnostic utilities (XML generation, project parsing, simulator)
+├── middleware/ # Ports & Adapters layer
+│ ├── shared/
+│ │ ├── ports/ # Port interfaces (platform-agnostic contracts)
+│ │ └── providers/ # PlatformContext (React Context for dependency injection)
+│ └── adapters/
+│ └── editor/ # Electron-specific port implementations (IPC bridge)
+├── types/ # Shared IPC type contracts
+└── __architecture__/ # Layer dependency validation script
+```
+
+### Ports & Adapters Pattern
+
+The codebase uses **dependency inversion** via port interfaces. Frontend code never imports backend or Electron APIs directly. All platform-specific behavior flows through ports.
+
+**Port interfaces** (`src/middleware/shared/ports/`):
+
+| Port | Responsibility |
+|------|---------------|
+| `CompilerPort` | PLC compilation pipeline |
+| `RuntimePort` | Remote PLC runtime control (login, start/stop, status) |
+| `DebuggerPort` | Debug protocol (read/write variables, MD5 verification) |
+| `SimulatorPort` | Built-in AVR simulator |
+| `ProjectPort` | Project CRUD operations |
+| `DevicePort` | Board discovery, serial ports |
+| `OrchestratorPort` | Device fleet management (web-only) |
+| `SystemPort` | Platform services (store, logging, external links) |
+| `WindowPort` | Native window management |
+| `AcceleratorPort` | Keyboard shortcuts |
+| `ThemePort` | Theme detection and switching |
+| `VersionControlPort` | Git operations |
+| `AIPort` | AI assistant (optional) |
+
+**Consuming ports** in components:
+```typescript
+import { useCompiler, useRuntime, useCapabilities } from '@root/middleware/shared/providers'
+
+function MyComponent() {
+ const compiler = useCompiler()
+ const capabilities = useCapabilities()
+
+ if (capabilities.hasLocalSerialPorts) { /* ... */ }
+ await compiler.compileProgram(args, onProgress)
+}
+```
+
+**Wiring** happens at the app root (`src/App.tsx`):
+```typescript
+import { editorPorts } from './middleware/editor-platform'
+...
+```
+
+**Editor adapters** (`src/middleware/adapters/editor/`) implement ports by calling `window.bridge.*` (Electron IPC). The web repo has its own adapters using HTTP/WebRTC instead.
+
+### Architecture Layer Rules
+
+Enforced by `npm run validate:arch`. Source dependencies point inward only:
-# Development mode (hot reload for both main and renderer)
-npm run dev
+```
+assets -> utils, data
+utils -> utils, ports, data, assets
+data -> ports, utils, data, assets
+types -> store, utils
+ports -> utils, ports
+provider -> ports, utils
+adapters -> ports, provider, utils, backend-shared, backend-web, store, assets
+backend-shared -> ports, utils, types
+store -> ports, provider, store, utils, assets
+services -> ports, provider, store, services, utils, assets
+hooks -> ports, provider, store, hooks, services, utils, assets
+components -> ports, provider, store, hooks, services, components, data, utils, assets
+```
+
+### IPC Communication
-# Build for production
-npm run build
+Main and renderer processes communicate through typed IPC bridges:
-# Build individual processes
-npm run build:main # Electron main process
-npm run build:renderer # React renderer process
-npm run build:dll # Webpack DLL for faster dev builds
+- **Main bridge:** `src/main/modules/ipc/main.ts` — `MainProcessBridge` registers 50+ `ipcMain.handle()` handlers
+- **Renderer bridge:** `src/main/modules/ipc/renderer.ts` — async wrappers calling `ipcRenderer.invoke()`
+- **Preload:** `src/main/modules/preload/preload.ts` — exposes `window.bridge` via `contextBridge`
-# Package for distribution
-npm run package
+### State Management (Zustand)
-# Linting and formatting
-npm run lint # ESLint check
-npm run lint:fix # Auto-fix lint issues
-npm run format # Prettier formatting
+Single store composed of 19 slices (`src/frontend/store/`), accessed via auto-generated selector hooks:
-# Testing
-npm run test # Jest with coverage
-npm run test:unit # Jest watch mode
-npm run test:e2e # Playwright E2E tests
+```typescript
+import { useOpenPLCStore } from '@root/frontend/store'
-# Native module rebuild (after node version changes)
-npm run rebuild
+const pous = useOpenPLCStore((s) => s.project.data.pous)
+const createPou = useOpenPLCStore((s) => s.projectActions.createPou)
```
-## Architecture Overview
+**Slice pattern** — each slice has three files:
-This is an Electron + React application for PLC (Programmable Logic Controller) programming using IEC 61131-3 standards.
+- `types.ts` — state shape + action signatures
+- `slice.ts` — implementation using Immer's `produce()` for immutable updates
+- `index.ts` — re-exports
-### Process Architecture
+**Key slices:**
+
+| Slice | Purpose |
+|-------|---------|
+| `project` | PLC project structure (POUs, data types, servers, devices) |
+| `device` | Board config, pin mappings, runtime connection |
+| `editor` | Editor models (discriminated union: textual, graphical, device, etc.) |
+| `tabs` | Open file tabs |
+| `workspace` | UI viewport state, debug values |
+| `ladder` | Ladder diagram rungs per POU |
+| `fbd` | FBD flow graphs per POU |
+| `console` | Log output |
+| `library` | System + user function block libraries |
+| `file` | File save states (dirty tracking) |
+| `ai`, `clipboard`, `history`, `modal`, `search`, `shared`, `version-control`, `webrtc` | Supporting features |
+
+**Conventions:**
+- Actions are grouped under a `*Actions` namespace (e.g., `projectActions`, `deviceActions`)
+- Complex actions return `{ ok: boolean; message?: string }` response objects
+- State is never mutated directly — always use `produce()` from Immer
+- Direct state access outside React: `openPLCStoreBase.getState()`
+
+### Component Organization (Atomic Design)
```
-Main Process (Node.js) Renderer Process (React)
-├── src/main/main.ts ├── src/renderer/index.tsx
-├── modules/ ├── components/
-│ ├── compiler/ ├── screens/
-│ ├── ipc/ │ ├── StartScreen/
-│ ├── modbus/ │ └── WorkspaceScreen/
-│ ├── preload/ ├── store/ (Zustand slices)
-│ └── websocket/ └── hooks/
-├── services/
-│ ├── project-service/
-│ ├── pou-service/
-│ └── user-service/
+src/frontend/components/
+├── _atoms/ # Primitive UI elements (buttons, inputs, select, checkbox, table)
+├── _molecules/ # Composed patterns (menu-bar, modal, variables-table, tabs)
+├── _organisms/ # Complex sections (explorer, panel, console, debugger, navigation)
+├── _features/ # Context-specific feature bundles
+│ ├── [app]/ # App-level (loading overlay, toast)
+│ ├── [start]/ # Start screen (menu, new-project modal)
+│ └── [workspace]/ # Workspace features
+│ └── editor/ # Monaco, graphical (LD/FBD/SFC), device, server editors
+├── _templates/ # Layout wrappers (app-layout, workspace-layout)
+└── ui/ # Radix UI primitive wrappers
```
-### IPC Communication
+### Navigation
-Communication between main and renderer uses typed IPC bridges:
-- **Main bridge:** `src/main/modules/ipc/main.ts` - Handler definitions
-- **Renderer bridge:** `src/main/modules/ipc/renderer.ts` - Invocation wrappers
-- **Preload:** `src/main/modules/preload/preload.ts` - Exposes `window.bridge`
+There is **no URL-based router**. Navigation is tab-driven via the Zustand `tabs` + `editor` slices:
-### State Management
+1. `App.tsx` renders `StartScreen` (no project) or `WorkspaceScreen` (project loaded)
+2. Opening a POU/resource creates a tab entry in the store
+3. Clicking a tab sets the active `EditorModel` (discriminated union determines which editor renders)
-Zustand store with 14 domain slices in `src/renderer/store/slices/`:
-- `WorkspaceSlice`, `EditorSlice`, `TabsSlice` - UI state
-- `FBDFlowSlice`, `LadderFlowSlice` - Visual programming editors
-- `ProjectSlice`, `FileSlice`, `DeviceSlice` - Project data
-- `ConsoleSlice`, `ModalSlice`, `SearchSlice` - Utilities
+### Graphical Editors
-### PLC Compilation Pipeline
+- **Ladder Diagram (LD):** DnD Kit-based, rung structure with contacts/coils/blocks
+- **Function Block Diagram (FBD):** @xyflow/react flow graph with custom node types (block, variable, connector, comment)
+- **SFC:** @xyflow/react graph (sequential function charts)
-The `CompilerModule` (`src/main/modules/compiler/`) orchestrates:
-1. IEC 61131-3 XML parsing
-2. Device/pin configuration generation
-3. C/C++ code generation via `xml2st` and `iec2c` binaries
-4. Arduino CLI integration for embedded targets
-5. Modbus TCP/RTU configuration
+Flow state is stored per-POU in dedicated slices (`ladder`, `fbd`). Flows must be relinked to current variables after variable table changes.
-Platform-specific binaries in `/resources/bin/[platform]/[arch]/`.
+### Compilation Pipeline
-### Key Path Aliases (tsconfig.json)
+Orchestrated by `CompilerModule` (`src/backend/editor/compiler/compiler-module.ts`):
```
-@root/* → ./src/*
-@process:main/* → ./src/main/*
-@process:renderer/* → ./src/renderer/*
-@components/* → ./src/renderer/components/*
-@utils/* → ./src/utils/*
-@shared/* → ./src/shared/*
-@hooks/* → ./src/renderer/hooks/*
+PLCProjectData -> Preprocess POUs -> XML Generation -> xml2st -> iec2c -> C code
+ |
+ defines.h (pins, Modbus, MD5)
+ |
+ Arduino CLI / openplc-compiler -> firmware
```
+Platform-specific binaries in `/resources/bin/[platform]/[arch]/`. Board configs in `/resources/sources/boards/hals.json`.
+
+### Debugging
+
+- **Protocol:** Custom Modbus PDU (function codes 0x41-0x45) for variable read/write
+- **Transports:** Modbus TCP, Modbus RTU, WebSocket, or virtual serial (simulator)
+- **Simulator:** AVR8JS emulator (`src/backend/shared/simulator/`) emulates ATmega2560
+- **Flow:** Compile with debug symbols (.dbg file + MD5) -> connect debugger -> poll variables
+
## Testing
-- **Unit tests:** Jest with jsdom environment, test files use `*.test.ts(x)` or `*.spec.ts(x)`
-- **E2E tests:** Playwright in `/e2e` directory, Chromium only
-- **Mocks:** `configs/mocks/` for file stubs, `identity-obj-proxy` for CSS modules
+- **Framework:** Jest + jsdom
+- **Test files:** `*.test.ts(x)`, `*.spec.ts(x)`, or `__tests__/` directories
+- **E2E:** Playwright (`/e2e`), Chromium only
+- **Coverage thresholds** (100% functions/lines/statements required):
+ - `src/frontend/store/slices/`
+ - `src/frontend/utils/`
+ - `src/backend/shared/`
+ - `src/middleware/adapters/editor/`
+- **Mocks:** `configs/mocks/` for file stubs; `identity-obj-proxy` for CSS modules
+
+When adding new code to covered directories, you must add corresponding tests to maintain 100% coverage.
## Code Style
-- This is a TypeScript-first codebase. Use strict TypeScript patterns, proper typing, and avoid `any` types.
+- TypeScript strict mode, avoid `any` types
- ESLint flat config (`eslint.config.mjs`) with TypeScript strict type checking
- Prettier: 120 char width, no semicolons, single quotes, trailing commas
- Import sorting enforced via `simple-import-sort` plugin
- Pre-commit hooks via Husky run lint-staged on `./src/**/*`
+- Path alias: `@root/*` -> `./src/*`
## Key Technologies
-- **Electron 35** / **React 18** / **TypeScript 5.4**
-- **Monaco Editor** for code editing (Python LSP support)
-- **Tailwind CSS** + **Radix UI** for styling
-- **DND Kit** for drag-and-drop in visual editors
+- **Electron 35** / **React 18** / **TypeScript** (target ES2022)
+- **Webpack** (not Vite) with separate main/renderer/preload configs
+- **Zustand 5** + **Immer** for state management
+- **Monaco Editor** for code editing (ST, IL, Python, C++)
+- **@xyflow/react 12** for FBD/SFC graphical editors
+- **@dnd-kit** for drag-and-drop (tabs, ladder rungs)
+- **Tailwind CSS 3** + **Radix UI** for styling
+- **Zod** for schema validation
+- **i18next** for internationalization
+- **avr8js** for Arduino simulation
+- **Axios** for HTTP requests
- **Socket.io** for real-time communication
-- **Modbus TCP/RTU** clients for industrial protocols
-
-## Debugging
-
-- When fixing UI flickering or rendering issues, always check for multiple potential causes: component re-renders, data changes, AND layout/sizing recalculations. Test fixes with the specific user-reported conditions (e.g., specific chart sizes, time ranges) before marking complete.
-- For multi-step debugging tasks, verify each fix is working before moving to the next issue. Run the app and confirm the specific behavior is resolved.
-
-## Environment Requirements
-
-- Node.js >= 20.x < 24
-- npm >= 10.x
-- Supported platforms: macOS, Windows, Linux (x64 & ARM64)
+- **Winston** for structured logging (main process)
+- **serialport** for serial communication
+
+## Important Patterns
+
+### When adding a new port:
+1. Define the interface in `src/middleware/shared/ports/`
+2. Add it to `PlatformPorts` in `src/middleware/shared/providers/types.ts`
+3. Add a convenience hook in `src/middleware/shared/providers/platform-context.tsx`
+4. Implement the editor adapter in `src/middleware/adapters/editor/`
+5. Wire it in `src/middleware/editor-platform.ts`
+
+### When adding a new store slice:
+1. Create `types.ts`, `slice.ts`, `index.ts` in `src/frontend/store/slices//`
+2. Add the slice type to `RootState` union in `src/frontend/store/index.ts`
+3. Spread the slice creator in `createOpenPLCStore()`
+4. Add tests to maintain 100% coverage
+
+### When adding a new POU language or type:
+1. Update project parser (`src/backend/shared/utils/parse-project-files.ts`)
+2. Update serializer for save flow
+3. Add editor component if graphical
+4. Register in library system and project actions
+
+### When modifying graphical editors:
+1. Flow state is stored separately from POU body during editing
+2. Sync flows back to POU on save
+3. Relink variables after variable table changes
+4. Node IDs must be unique per flow
+
+## Environment
+
+- **Node.js:** >= 20.x < 24
+- **Dev server port:** 1313
+- **Supported platforms:** macOS, Windows, Linux (x64 & ARM64)
+- **Binaries:** Auto-downloaded via `scripts/download-binaries.ts` during `npm install`
diff --git a/README.md b/README.md
index 1d6ead46c..8861e05c6 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ In order to run the development version, clone the repository, and install depen
You'll need the following tools:
- [Git](https://git-scm.com/)
-- [NodeJS](https://nodejs.org/en/download/), **x64**, version `>=20`
+- [NodeJS](https://nodejs.org/en/download/), version `>=20 <24`
### Step by step
diff --git a/docs/migration-state.md b/docs/migration-state.md
deleted file mode 100644
index 07ca5e84a..000000000
--- a/docs/migration-state.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Migration State
-
-Tracks progress of the shared UI migration. Updated by the `/migrate` skill after each step.
-
-## Current Phase
-
-integration
-
-## Current Step
-
-31
-
-## Step Log
-
-| Step | Phase | Description | Status | Date |
-|------|-------|-------------|--------|------|
-| 0 | planning | Migration tracker, port interfaces, PlatformProvider scaffolding | done | 2026-03-10 |
-| 1 | architecture | Define clean architecture layers and create validation tests | done | 2026-03-10 |
-| 2 | domain | Migrate shared domain types and pure utilities | done | 2026-03-10 |
-| 3 | adapters | ThemePort adapter implementation | done | 2026-03-10 |
-| 4 | adapters | SystemPort adapter implementation | done | 2026-03-10 |
-| 5 | adapters | WindowPort adapter implementation | done | 2026-03-10 |
-| 6 | adapters | AcceleratorPort adapter implementation | done | 2026-03-10 |
-| 7 | adapters | DevicePort adapter implementation | done | 2026-03-10 |
-| 8 | adapters | ProjectPort adapter implementation | done | 2026-03-10 |
-| 9 | adapters | CompilerPort adapter implementation | done | 2026-03-10 |
-| 10 | adapters | RuntimePort adapter implementation | done | 2026-03-10 |
-| 11 | adapters | DebuggerPort adapter implementation | done | 2026-03-10 |
-| 12 | adapters | SimulatorPort adapter implementation | done | 2026-03-10 |
-| 13 | store | UI state slices (Workspace, Editor, Tabs, Modal, Search) | done | 2026-03-10 |
-| 14 | store | Data state slices (Project, File, Library, Console, Shared) | done | 2026-03-10 |
-| 15 | store | Visual editor slices (FBDFlow, LadderFlow) | done | 2026-03-11 |
-| 16 | store | Platform state slices (Device, History, web-only) | done | 2026-03-11 |
-| 17 | resources | Copy shared resources (styles, assets, locales, declarations) to src2/ | done | 2026-03-11 |
-| 18 | components | Atoms batch 1 — shared identical components (revised: 23 kept, 17 divergent moved to step 22) | done | 2026-03-11 |
-| 19 | architecture-rework | Restructure src2/ into frontend/middleware/backend three-layer architecture | done | 2026-03-11 |
-| 20 | architecture-rework | Extract application logic from Zustand stores into backend/shared/ | done | 2026-03-11 |
-| 21 | architecture-rework | Update comparison script to validate all byte-identical surfaces | done | 2026-03-11 |
-| 22 | components | Atoms batch 2a — simple divergent atoms, generic-table-inputs, UI scroll-area | done | 2026-03-11 |
-| 22b | components | Atoms batch 2b — graphical editor divergent atoms + platform-specific atoms | done | 2026-03-11 |
-| 23 | components | Molecules batch 1 — shared identical (10 modules migrated, divergent deferred to step 24) | done | 2026-03-11 |
-| 24 | components | Molecules batch 2a — divergent non-graphical-editor molecules | done | 2026-03-12 |
-| 24b | components | Molecules batch 2b — graphical-editor divergent molecules (fbd/ladder utils, fbd-utils, rung, index) | done | 2026-03-12 |
-| 25 | components | Organisms — shared (15 files: console, explorer, global-variables-editor, graphical-editor/ladder, navigation, panel, plc-logs, variables-code-editor, workspace-activity-bar) | done | 2026-03-12 |
-| 26 | components | Organisms — platform-specific (port-dependent) | done | 2026-03-12 |
-| 27 | components | Features — shared (45 files: toast, menu, new-project store/interval-model/first-step, graphical editor routing/SFC, device configuration + pin-mapping-table, monaco completion/configs/languages/themes/drag-and-drop, server barrel + opcua-server, create-element data-type-element, arrow-button-group, fbd/ladder block library, ladder coil/contact) | done | 2026-03-12 |
-| 28 | components | Features — platform-specific (97 files: divergent reconciliation of search, create-element, device/board, monaco editor, data-type editor; web-only AI chat, debug-manager, loading-overlay, orchestrators, device elements; editor-only device-aware gating; new utils device.ts, formatters/POU.ts, data sources data-type.tsx) | done | 2026-03-13 |
-| 29 | components | Templates, screens, and shared hooks | done | 2026-03-13 |
-| 30 | components | Hooks migration — debug/AI/WebRTC hooks, services, utilities, simulator facade, architecture validator updates (637 byte-identical files) | done | 2026-03-13 |
-| 31 | integration | App shell and routing — editor store-based routing, web TanStack Router with project loading | done | 2026-03-13 |
-| 32 | integration | Production build configs | pending | |
-| 33 | integration | Switchover and cleanup — remove src/ | pending | |
diff --git a/docs/migration-tracker.md b/docs/migration-tracker.md
deleted file mode 100644
index 0690d026a..000000000
--- a/docs/migration-tracker.md
+++ /dev/null
@@ -1,484 +0,0 @@
-# Shared UI Migration Tracker
-
-Base branches: `refactor/shared-ui-migration` on both repos
-- Editor: `4a9f4001` (from `origin/development`)
-- Web: `5130d60` (from `origin/development`)
-
-Legend:
-- **S** = Shared (identical or <5% diff, pure UI)
-- **D** = Divergent (>5% diff, needs reconciliation)
-- **E** = Editor only
-- **W** = Web only
-- **P** = Platform-specific (needs port interface)
-- Status: `[ ]` pending, `[~]` in progress, `[x]` migrated
-
----
-
-## 1. ATOMS (`_atoms/`)
-
-### Identical (copy as-is)
-| # | Component | Status |
-|---|-----------|--------|
-| 1 | `accordion/index.tsx` | [x] S |
-| 2 | `buttons/index.ts` | [x] S |
-| 3 | `buttons/default/index.tsx` | [x] S |
-| 4 | `buttons/window-control/index.tsx` | [x] S |
-| 5 | `card/index.tsx` | [x] S |
-| 6 | `dimensions-modal/array-dimensions-input/index.tsx` | [x] S |
-| 7 | `graphical-editor/fbd/svg/connector-svg.tsx` | [x] S |
-| 8 | `graphical-editor/fbd/svg/continuation-svg.tsx` | [x] S |
-| 9 | `graphical-editor/fbd/svg/index.ts` | [x] S |
-| 10 | `input/index.tsx` | [x] S |
-| 11 | `label/index.tsx` | [x] S |
-| 12 | `tab-list/index.tsx` | [x] S |
-| 13 | `tooltip/index.tsx` | [x] S |
-| 14 | `workspace-activity-bar/divider.tsx` | [x] S |
-
-### Near-identical (<5% diff, import paths and cosmetic only)
-| # | Component | Status |
-|---|-----------|--------|
-| 15 | `buttons/activity-bar/index.tsx` | [x] S |
-| 16 | `buttons/console/clear-console.tsx` | [x] S |
-| 17 | `buttons/tables-actions/index.tsx` | [x] S |
-| 18 | `file/index.tsx` | [x] S |
-| 19 | `generic-table/index.tsx` | [x] S |
-| 20 | `graphical-editor/fbd/handle.tsx` | [x] S |
-| 21 | `react-flow/index.tsx` | [x] S |
-| 22 | `table/index.tsx` | [x] S |
-| 23 | `type-dropdown-selector/index.tsx` | [x] S |
-
-### Divergent (needs reconciliation)
-| # | Component | Diff | Status | Notes |
-|---|-----------|------|--------|-------|
-| 24 | `checkbox/index.tsx` | 456B | [x] D | Reconciled: web superset (label, disabled, checked border) |
-| 25 | `debug-tree-node/index.tsx` | 14 | [x] D | Reconciled: unified imports |
-| 26 | `dimensions-modal/index.tsx` | 28 | [x] D | Reconciled: unified imports, PLCBaseType |
-| 27 | `generic-data-type-table/index.tsx` | 258B | [x] D | Reconciled: web scroll wrapper |
-| 28 | `generic-table-inputs/*` | varies | [x] D | Reconciled: editor superset (5 files), search via extractSearchQuery |
-| 29 | `graphical-editor/autocomplete/index.tsx` | 63 | [ ] D | Reclassified: editor has triggerSubmit imperative handle |
-| 30 | `graphical-editor/fbd/autocomplete/index.tsx` | 43 | [ ] D | Reclassified: different ID gen, error handling |
-| 31 | `graphical-editor/fbd/block.tsx` | 7.4KB | [ ] D | Editor much larger |
-| 32 | `graphical-editor/fbd/comment.tsx` | 1.4KB | [ ] D | |
-| 33 | `graphical-editor/fbd/connection.tsx` | 2KB | [ ] D | |
-| 34 | `graphical-editor/fbd/index.ts` | 14 | [ ] D | Reclassified: web uses buildNodes module |
-| 35 | `graphical-editor/fbd/utils/index.ts` | - | [ ] D | Barrel for types.ts + utils.ts |
-| 36 | `graphical-editor/fbd/utils/types.ts` | 1.2KB | [ ] D | Web much larger |
-| 37 | `graphical-editor/fbd/utils/utils.ts` | 3.6KB | [ ] D | Web much larger |
-| 38 | `graphical-editor/fbd/variable.tsx` | 2.9KB | [ ] D | |
-| 39 | `graphical-editor/ladder/autocomplete/index.tsx` | 44 | [ ] D | Reclassified: different ID gen, error handling |
-| 40 | `graphical-editor/ladder/block.tsx` | 10.6KB | [ ] D | Editor much larger |
-| 41 | `graphical-editor/ladder/coil.tsx` | 5KB | [ ] D | |
-| 42 | `graphical-editor/ladder/contact.tsx` | 4.3KB | [ ] D | |
-| 43 | `graphical-editor/ladder/handle.tsx` | 16 | [ ] D | Reclassified: editor has extra props |
-| 44 | `graphical-editor/ladder/index.ts` | 152 | [ ] D | Reclassified: web uses constants/buildNodes modules |
-| 45 | `graphical-editor/ladder/mock-node.tsx` | 2.9KB | [ ] D | Editor much larger |
-| 46 | `graphical-editor/ladder/parallel.tsx` | 3.8KB | [ ] D | Editor much larger |
-| 47 | `graphical-editor/ladder/placeholder.tsx` | 2.4KB | [ ] D | Editor much larger |
-| 48 | `graphical-editor/ladder/power-rail.tsx` | 2.6KB | [ ] D | Editor much larger |
-| 49 | `graphical-editor/ladder/utils/index.ts` | - | [ ] D | Barrel for types.ts + utils.ts |
-| 50 | `graphical-editor/ladder/utils/types.ts` | 3.4KB | [ ] D | Web much larger |
-| 51 | `graphical-editor/ladder/utils/utils.ts` | 8KB | [ ] D | Web much larger |
-| 52 | `graphical-editor/ladder/variable.tsx` | 2.6KB | [ ] D | |
-| 53 | `graphical-editor/types/block.ts` | 9 | [ ] D | Reclassified: depends on divergent Zod schemas |
-| 54 | `graphical-editor/utils/index.ts` | 14 | [ ] D | Reclassified: different type/import sources |
-| 55 | `highlighted-textarea/index.tsx` | 19 | [x] D | Reconciled: web approach (extractSearchQuery, no HighlightedText dep) |
-| 56 | `react-flow/style.css` | 24 | [x] D | Reconciled: web class-based dark mode |
-| 57 | `select/index.tsx` | 405B | [x] D | Reconciled: editor superset (forwardRef, viewportRef) |
-| 58 | `tab/index.tsx` | 74 | [x] D | Reconciled: merged web icons + editor safe rendering |
-| 59 | `table-actions/index.tsx` | 14 | [x] D | Reconciled: editor superset (className prop) |
-
-### Platform-specific
-| # | Component | Repo | Status | Notes |
-|---|-----------|------|--------|-------|
-| 60 | `highlighted-text/index.tsx` | E | [ ] E | Editor only |
-| 61 | `graphical-editor/debug-value-badge.tsx` | E | [ ] E | Editor only |
-| 62 | `graphical-editor/block-output-debug-badges.tsx` | E | [ ] E | Editor only |
-| 63 | `resolution-warning-message/index.tsx` | W | [ ] W | Web only |
-| 64 | `react-flow/custom-nodes/coil.tsx` | W | [ ] W | Web only |
-| 65 | `react-flow/custom-nodes/contact.tsx` | W | [ ] W | Web only |
-| 66 | `graphical-editor/fbd/buildNodes.tsx` | W | [ ] W | Web only |
-| 67 | `graphical-editor/ladder/buildNodes.tsx` | W | [ ] W | Web only |
-| 68 | `graphical-editor/fbd/utils/constants.tsx` | W | [ ] W | Web only |
-| 69 | `graphical-editor/ladder/utils/constants.tsx` | W | [ ] W | Web only |
-
----
-
-## 2. MOLECULES (`_molecules/`)
-
-### Identical / near-identical
-| # | Component | Status |
-|---|-----------|--------|
-| 70 | `file/*` (4 files) | [x] S |
-| 71 | `pin-mapping-table/*` (4 files) | [x] S |
-| 72 | `select-field/index.tsx` | [x] S |
-| 73 | `input-field/index.tsx` | [x] S |
-| 74 | `toast/index.tsx` | [x] S |
-| 75 | `window-controls/index.tsx` | [ ] E | Reclassified: editor-only (uses window.bridge) |
-| 76 | `menu-bar/index.tsx` | [x] D | Reconciled step 24: shell + stub menus |
-| 77 | `modal/index.tsx` | [x] S |
-| 78 | `rename-impact-modal/index.tsx` | [x] S |
-| 79 | `type-change-modal/index.tsx` | [x] D | Reconciled step 24: validation extracted to backend/shared |
-| 80 | `breadcrumbs/index.tsx` | [x] S |
-| 81 | `search/index.tsx` | [x] S |
-| 82 | `variables-panel/index.tsx` | [x] D | Reconciled step 24 |
-| 83 | `data-types/array/* (header + table)` | [x] D | Reconciled step 24 |
-| 84 | `data-types/enumerated/*` | [x] D | Reconciled step 24 |
-| 85 | `data-types/structure/* (table + elements)` | [x] D | Reconciled step 24 |
-| 86 | `graphical-editor/fbd/fbd-utils/*` | [ ] D | Reclassified: useCopyPaste.ts divergent |
-| 87 | `graphical-editor/fbd/index.tsx` | [ ] D | Reclassified: heavily divergent (ID gen, node sync, delete logic) |
-| 88 | `graphical-editor/ladder/rung/index.tsx` | [ ] D | Reclassified: rung children divergent |
-| 89 | `graphical-editor/ladder/index.tsx` | [ ] D | Reclassified: depends on divergent children |
-| 90 | `graphical-editor/ladder/* utils` | [ ] D | Reclassified: ID generation differs (newGraphicalEditorNodeID vs uuidv4) |
-| 91 | `instances-table/*` | [x] D | Reconciled step 24 |
-| 92 | `library-tree/index.tsx` | [x] S |
-| 93 | `task-table/*` | [x] D | Reconciled step 24 |
-| 94 | `global-variables-table/elements/*` | [x] D | Reconciled step 24 |
-| 95 | `variables-table/elements/*` | [x] D | Reconciled step 24 |
-
-### Divergent
-| # | Component | Diff | Status | Notes |
-|---|-----------|------|--------|-------|
-| 96 | `charts/line-chart.tsx` | 50% | [x] D | Reconciled step 24 |
-| 97 | `global-variables-table/editable-cell.tsx` | 21% | [x] D | Reconciled step 24 |
-| 98 | `global-variables-table/selectable-cell.tsx` | 30% | [x] D | Reconciled step 24 |
-| 99 | `variables-table/editable-cell.tsx` | 48% | [x] D | Reconciled step 24 |
-| 100 | `tabs/index.tsx` | 23% | [x] D | Reconciled step 24 |
-| 101 | `graphical-editor/ladder/rung/body.tsx` | 13% | [ ] D | Deferred to step 24b |
-| 102 | `workspace-activity-bar/download.tsx` | 47% | [x] D | Reconciled step 24 |
-
-### Platform-specific (menus)
-| # | Component | Status | Notes |
-|---|-----------|--------|-------|
-| 103 | `menu-bar/menus/display.tsx` | [ ] P | 25% diff, platform behavior |
-| 104 | `menu-bar/menus/file.tsx` | [ ] P | 23% diff, Electron file ops vs web |
-| 105 | `menu-bar/menus/recent.tsx` | [ ] P | 78% diff, Electron recent files |
-| 106 | `project-tree/utils/index.ts` | [ ] W | Web only (extracted util) |
-| 107 | `workspace-activity-bar/default/chat.tsx` | [ ] W | Web only (AI chat button) |
-
----
-
-## 3. ORGANISMS (`_organisms/`)
-
-### Identical / near-identical
-| # | Component | Status |
-|---|-----------|--------|
-| 107 | `console/* (3 files)` | [x] S |
-| 108 | `explorer/index.tsx` | [x] S |
-| 109 | `explorer/info.tsx` | [x] S |
-| 110 | `global-variables-editor/index.tsx` | [x] S |
-| 111 | `graphical-editor/ladder/index.ts` | [x] S |
-| 112 | `graphical-editor/ladder/rung/index.tsx` | [x] S |
-| 113 | `navigation/index.tsx` | [x] S |
-| 114 | `panel/index.tsx` | [x] S |
-| 115 | `plc-logs/* (2 files)` | [x] S |
-| 116 | `variables-code-editor/index.tsx` | [x] S |
-| 117 | `workspace-activity-bar/fbd-toolbox.tsx` | [x] S |
-| 118 | `workspace-activity-bar/index.tsx` | [x] S |
-
-### Divergent
-| # | Component | Diff | Status | Notes |
-|---|-----------|------|--------|-------|
-| 119 | `debugger/index.tsx` | 400B | [ ] D | Web slightly larger |
-| 120 | `explorer/library.tsx` | 400B | [ ] D | Web slightly larger |
-| 121 | `explorer/project.tsx` | 1.3KB | [ ] D | Editor larger |
-| 122 | `instances-editor/index.tsx` | 1KB | [ ] D | Editor larger |
-| 123 | `task-editor/index.tsx` | 1KB | [ ] D | Editor larger |
-| 124 | `variables-editor/index.tsx` | 11KB | [ ] D | Editor 27% larger |
-| 125 | `workspace-activity-bar/default.tsx` | 3KB | [ ] P | Business logic mixed in |
-| 126 | `workspace-activity-bar/ladder-toolbox.tsx` | 2.4KB | [ ] D | Web 68% larger |
-| 127 | `title-bar/index.tsx` | 112B | [ ] D | |
-| 128 | `title-bar/slots/*` | N/A | [ ] P | Completely different structure |
-
-### Platform-specific modals
-| # | Component | Repo | Status | Notes |
-|---|-----------|------|--------|-------|
-| 129 | `modals/debugger-message-modal.tsx` | Both | [ ] S | |
-| 130 | `modals/delete-confirmation-modal.tsx` | Both | [ ] D | Editor 1.3KB larger |
-| 131 | `modals/runtime-connection-lost-modal.tsx` | Both | [ ] S | |
-| 132 | `modals/runtime-create-user-modal.tsx` | Both | [ ] D | Web 1.3KB larger |
-| 133 | `modals/runtime-login-modal.tsx` | Both | [ ] P | Direct IPC/API calls |
-| 134 | `modals/save-changes-file-modal.tsx` | Both | [ ] S | |
-| 135 | `modals/save-changes-modal.tsx` | Both | [ ] D | Editor 1.4KB larger |
-| 136 | `modals/confirm-device-switch-modal.tsx` | E | [ ] E | |
-| 137 | `modals/debugger-ip-input-modal.tsx` | E | [ ] E | |
-| 138 | `modals/quit-application-modal.tsx` | E | [ ] E | Electron only |
-| 139 | `modals/server-ip-mismatch-modal.tsx` | W | [ ] W | |
-| 140 | `about-modal/index.tsx` | E | [ ] E | |
-| 141 | `display-recent-projects/index.tsx` | E | [ ] E | |
-| 142 | `project-filter-bar/index.tsx` | E | [ ] E | |
-| 143 | `pin-mapping-editor/index.tsx` | W | [ ] W | |
-
----
-
-## 4. FEATURES (`_features/`)
-
-### [app]
-| # | Component | Repo | Status | Notes |
-|---|-----------|------|--------|-------|
-| 144 | `toast/*` (3 files) | Both | [ ] S | Near-identical |
-| 145 | `debug-manager/index.tsx` | W | [ ] W | 16KB, simulator/debugger UI |
-| 146 | `loading-overlay/index.tsx` | W | [ ] W | |
-
-### [start]
-| # | Component | Status | Notes |
-|---|-----------|--------|-------|
-| 147 | `menu/index.tsx` | [ ] S | Identical |
-| 148 | `new-project/interval-model.tsx` | [ ] S | |
-| 149 | `new-project/project-modal.tsx` | [ ] S | |
-| 150 | `new-project/steps/first-step.tsx` | [ ] S | |
-| 151 | `new-project/steps/second-step.tsx` | [ ] D | Different implementations |
-| 152 | `new-project/steps/third-step.tsx` | [ ] D | Different implementations |
-| 153 | `new-project/store/index.ts` | [ ] S | |
-
-### [workspace]/editor
-| # | Component | Status | Notes |
-|---|-----------|--------|-------|
-| 154 | `editor/device/index.tsx` | [ ] S | |
-| 155 | `editor/device/configuration/index.tsx` | [ ] S | |
-| 156 | `editor/device/configuration/communication.tsx` | [ ] S | |
-| 157 | `editor/device/configuration/board.tsx` | [ ] D | Editor 702 lines vs Web 294 lines |
-| 158 | `editor/device/configuration/components/*` | [ ] S | modbus-rtu, modbus-tcp, pin-mapping, static-host |
-| 159 | `editor/device/remote-device/index.tsx` | [ ] S | ~45KB both |
-| 160 | `editor/device/orchestrators/*` | W | [ ] W | Web only (32KB orchestrator list) |
-| 161 | `editor/device/components/*` | W | [ ] W | Web only (extracted components) |
-| 162 | `editor/device/elements/*` | W | [ ] W | Web only (board-config, tcp-settings, rtu-settings) |
-| 163 | `editor/graphical/index.tsx` | [ ] S | |
-| 164 | `editor/graphical/FBD/index.tsx` | [ ] S | |
-| 165 | `editor/graphical/SFC/index.tsx` | [ ] S | |
-| 166 | `editor/graphical/elements/*` | [ ] S | Block, coil, contact, arrow-button-group |
-| 167 | `editor/graphical/ladder/index.tsx` | E | [ ] E | 12KB monolithic (web refactored out) |
-| 168 | `editor/monaco/index.tsx` | [ ] D | Editor 1167 lines vs Web 823 lines |
-| 169 | `editor/monaco/completion/*` | [ ] S | All completion files identical |
-| 170 | `editor/monaco/configs/*` | [ ] S | All language/theme configs identical |
-| 171 | `editor/monaco/drag-and-drop/*` | [ ] S | |
-| 172 | `editor/monaco/python-lsp/*` | [ ] S | |
-| 173 | `editor/monaco/theme-utils.ts` | W | [ ] W | Web only |
-| 174 | `editor/resource-editor/index.tsx` | [ ] S | |
-| 175 | `editor/search-in-project/index.tsx` | [ ] S | |
-| 176 | `editor/server/index.ts` | [ ] S | |
-| 177 | `editor/server/modbus-server/index.tsx` | [ ] S | |
-| 178 | `editor/server/opcua-server/*` | [ ] S | All identical |
-| 179 | `editor/server/s7comm-server/index.tsx` | [ ] S | |
-| 180 | `editor/server/modbus-server/address-mapping-reference.tsx` | W | [ ] W | Web only |
-| 181 | `editor/data-type/index.tsx` | [ ] D | Web 180% larger |
-| 182 | `create-element/*` | [ ] S | |
-| 183 | `create-element/hooks/use-name-validation.ts` | W | [ ] W | Web only |
-| 184 | `search/*` | [ ] S | |
-| 185 | `ai-chat/*` (5 files) | W | [ ] W | AI chat panel, input, messages, code block (web only) |
-| 186 | `editor/monaco/ai-completion/*` (3 files) | W | [ ] W | AI inline completion provider, context builder |
-| 187 | `editor/monaco/ai-consent-modal.tsx` | W | [ ] W | AI consent dialog |
-| 188 | `editor/monaco/ai-status-indicator.tsx` | W | [ ] W | AI status indicator |
-
----
-
-## 5. TEMPLATES (`_templates/`)
-
-| # | Component | Status | Notes |
-|---|-----------|--------|-------|
-| 185 | `[start]/index.ts` | [ ] S | |
-| 186 | `[start]/main-content.tsx` | [ ] S | |
-| 187 | `[start]/side-content.tsx` | [ ] S | |
-| 188 | `[workspace]/index.ts` | [ ] S | |
-| 189 | `[workspace]/side-content.tsx` | [ ] S | |
-| 190 | `[workspace]/main-content.tsx` | [ ] D | Web 29% smaller |
-| 191 | `[editors]/index.ts` | [ ] S | |
-| 192 | `[editors]/device-editor-slot.tsx` | [ ] S | |
-| 193 | `[editors]/device-editor-template.tsx` | [ ] S | |
-| 194 | `app-layout.tsx` | [ ] D | Minor styling diff |
-| 195 | `accelerator-handler.tsx` | E | [ ] E | Electron keyboard shortcuts |
-
----
-
-## 6. SCREENS / PAGES
-
-| # | Component | Repo | Status | Notes |
-|---|-----------|------|--------|-------|
-| 196 | `workspace-screen.tsx` | Both | [ ] P | Editor 2072 lines vs Web 604 lines. Heaviest business logic. |
-| 197 | `start-screen.tsx` | E | [ ] E | |
-| 198 | `welcome-page.tsx` | W | [ ] W | |
-| 199 | `unauthorized-page.tsx` | W | [ ] W | Web auth page |
-
----
-
-## 7. HOOKS
-
-### Shared
-| # | Hook | Status | Notes |
-|---|------|--------|-------|
-| 200 | `use-debug-composite-key.ts` | [ ] S | Identical |
-| 201 | `use-remove-tab.tsx` | [ ] S | Near-identical |
-| 202 | `use-store-selectors.ts` | [ ] D | Editor 258 lines vs Web 119 lines |
-
-### Platform-specific
-| # | Hook | Repo | Status | Notes |
-|---|------|------|--------|-------|
-| 203 | `use-compiler.ts` | E | [ ] P | Calls `window.bridge.exportProjectXml()` |
-| 204 | `use-quit-app.tsx` | E | [ ] E | Electron only |
-| 205 | `use-runtime-polling.ts` | E | [ ] P | Calls `window.bridge.*` |
-| 206 | `useDebugPolling.ts` | W | [ ] P | Calls web debug bridge |
-| 207 | `useDebugSession.ts` | W | [ ] P | Debug session lifecycle |
-| 208 | `useDebuggerLauncher.ts` | W | [ ] P | 790 lines, compiler pipeline |
-| 209 | `useRuntimePolling.ts` | W | [ ] P | Calls web APIs |
-| 210 | `useSaveShortcut.ts` | W | [ ] W | |
-| 211 | `useUndoRedoShortcut.ts` | W | [ ] W | |
-| 212 | `useWebRTCConnection.ts` | W | [ ] W | WebRTC specific |
-| 213 | `useAI.ts` | W | [ ] W | AI chat and completion hook |
-
----
-
-## 8. STORE SLICES
-
-### Shared slices (in both repos)
-| # | Slice | Status | Notes |
-|---|-------|--------|-------|
-| 213 | `console/` | [ ] S | |
-| 214 | `editor/` | [ ] S | |
-| 215 | `fbd/` | [ ] S | |
-| 216 | `files/` | [ ] S | |
-| 217 | `ladder/` | [ ] D | Editor 4 files vs Web 3 |
-| 218 | `library/` | [ ] S | |
-| 219 | `modal/` | [ ] S | |
-| 220 | `project/` | [ ] D | Both 8 files, but complex business logic inside |
-| 221 | `react-flow/` | [ ] S | |
-| 222 | `search/` | [ ] S | |
-| 223 | `shared/` | [ ] S | |
-| 224 | `tabs/` | [ ] S | |
-| 225 | `workspace/` | [ ] D | Both 6 files, may mix platform state |
-| 226 | `device/` | [ ] P | Editor 7 files vs Web 5, runtime connection state differs |
-
-### Platform-specific slices
-| # | Slice | Repo | Status | Notes |
-|---|-------|------|--------|-------|
-| 227 | `history/` | E | [ ] E | Undo/redo |
-| 228 | `webrtc/` | W | [ ] W | WebRTC state |
-| 229 | `ai/` (3 files) | W | [ ] W | AI state (chat messages, completions, consent, telemetry) |
-
----
-
-## 9. UTILS
-
-### Shared utils
-| # | Utility | Status | Notes |
-|---|---------|--------|-------|
-| 229 | `debug-tree-builder.ts` | [ ] S | |
-| 230 | `parse-debug-file.ts` | [ ] S | |
-| 231 | `sync-nodes-with-variables.ts` | [ ] S | |
-| 232 | `validate-variable-reference.ts` | [ ] S | |
-| 233 | `variable-references.ts` | [ ] D | Editor 532 vs Web 562 lines |
-| 234 | `variable-sizes.ts` | [ ] D | Editor 217 vs Web 278 lines |
-| 235 | `debug-tree-traversal.ts` | [ ] D | Editor 394 vs Web 366 lines |
-| 236 | `debug-variable-finder.ts` | [ ] S | |
-| 237 | `keywords.ts` | [ ] S | |
-| 238 | `pou-helpers.ts` | [ ] S | |
-| 239 | `generate-iec-string-to-variables.ts` | [ ] D | Editor 190 vs Web 88 lines |
-| 240 | `remote-device-options.ts` | [ ] S | |
-
-### Utils subdirectories
-| # | Directory | Status | Notes |
-|---|-----------|--------|-------|
-| 241 | `PLC/` | [ ] D | Editor 29 files (codesys + old-editor), Web 14 files (consolidated) |
-| 242 | `cpp/` (5 files) | [ ] S | Same structure |
-| 243 | `python/` (5 files) | [ ] S | Same structure |
-| 244 | `modbus/` | [ ] D | Editor 2 files, Web 4 files |
-| 245 | `opcua/` | [ ] D | Editor 5 files, Web 4 files |
-| 246 | `s7comm/` (2 files) | [ ] S | Same structure |
-| 247 | `formatters/` | [ ] D | Editor 3 files, Web 2 files |
-
-### Editor-only utils
-| # | Utility | Status | Notes |
-|---|---------|--------|-------|
-| 248 | `debugger-session.ts` | [ ] P | 294 lines, IPC-coupled |
-| 249 | `PLC/pou-text-parser.ts` | [ ] E | 517 lines |
-| 250 | `PLC/pou-text-serializer.ts` | [ ] E | 148 lines |
-| 251 | `PLC/pou-file-extensions.ts` | [ ] E | 110 lines |
-| 252 | `PLC/preprocess-pous.ts` | [ ] E | 182 lines |
-| 253 | `PLC/array-codegen-helpers.ts` | [ ] E | 110 lines |
-| 254 | `PLC/codesys/*` | [ ] E | Separate XML format |
-| 255 | `PLC/old-editor/*` | [ ] E | Separate XML format |
-
-### Web-only utils
-| # | Utility | Status | Notes |
-|---|---------|--------|-------|
-| 256 | `cookies.ts` | [ ] W | |
-| 257 | `download-file.ts` | [ ] W | |
-| 258 | `hex.ts` | [ ] W | |
-| 259 | `library.ts` | [ ] W | |
-| 260 | `project-parser.ts` | [ ] W | 601 lines |
-| 261 | `project-serializer.ts` | [ ] W | 187 lines |
-| 262 | `project-summary.ts` | [ ] W | |
-| 263 | `server-ip-validation.ts` | [ ] W | |
-| 264 | `theme.ts` | [ ] W | |
-| 265 | `graphical/drag-detection.ts` | [ ] W | |
-| 266 | `graphical/relink-variables.ts` | [ ] W | |
-
----
-
-## 10. SHARED MODULES
-
-| # | Module | Repo | Status | Notes |
-|---|--------|------|--------|-------|
-| 267 | `data/index.ts` | Both | [ ] S | |
-| 268 | `data/constants.ts` | Both | [ ] S | |
-| 269 | `data/common.ts` | Both | [ ] S | |
-| 270 | `contracts/types/*` | E | [ ] E | pou.ts, xml-project.ts |
-| 271 | `contracts/validations/*` | E | [ ] E | pou-validation.ts, project-validation.ts |
-| 272 | `data/mock/*` | E | [ ] E | Test data |
-
----
-
-## 11. SERVICES (Web only)
-
-| # | Service | Status | Notes |
-|---|---------|--------|-------|
-| 273 | `api/axios.ts` | [ ] W | HTTP client config |
-| 274 | `api/compiler-api.ts` | [ ] W | Compiler endpoints |
-| 275 | `api/debug-transport.ts` | [ ] W | Debug protocol |
-| 276 | `api/project-api.ts` | [ ] W | Project CRUD |
-| 277 | `api/runtime-api.ts` | [ ] W | Runtime communication |
-| 278 | `api/webrtc/*` (5 files) | [ ] W | WebRTC signaling/connection |
-| 279 | `debug/debug-bridge.ts` | [ ] W | Debug protocol adapter |
-| 280 | `debug/types.ts` | [ ] W | |
-| 281 | `debug/transports/*` (2 files) | [ ] W | Modbus RTU + WebRTC transports |
-| 282 | `simulator/*` (6 files) | [ ] W | In-browser AVR simulator |
-| 283 | `debug-session-controls.ts` | [ ] W | Session lifecycle |
-| 284 | `ai/*` (6 files) | W | [ ] W | AI api-client, completion-cache, context-collector, telemetry, types |
-
----
-
-## 12. IPC BRIDGE (Editor only — to be replaced by port interfaces)
-
-The editor's `window.bridge.*` with 350+ methods is the main coupling point.
-These will be replaced by port interfaces that both repos implement differently.
-
-### Port interfaces needed (derived from IPC categories)
-| # | Port | Methods | Status |
-|---|------|---------|--------|
-| P1 | `CompilerPort` | compileProject, exportXml, getCompilationStatus | [ ] |
-| P2 | `RuntimePort` | login, getStatus, startPlc, stopPlc, getLogs, getSerialPorts, createUser | [ ] |
-| P3 | `DebuggerPort` | connect, disconnect, getVariablesList, setVariable, verifyMd5 | [ ] |
-| P4 | `SimulatorPort` | loadFirmware, stop, isRunning, onStopped | [ ] |
-| P5 | `ProjectPort` | openProject, saveProject, createPou, deletePou, renamePou, pickPath | [ ] |
-| P6 | `DevicePort` | getAvailableBoards, getCommunicationPorts, getPreviewImage | [ ] |
-| P7 | `SystemPort` | getSystemInfo, setStoreValue, getStoreValue, retrieveRecent | [ ] |
-| P8 | `WindowPort` | minimize, maximize, close, hide, reload, quit | [ ] |
-| P9 | `AcceleratorPort` | onSaveProject, onOpenProject, onUndo, onRedo, ... | [ ] |
-| P10 | `ThemePort` | getCurrentTheme, setTheme, onThemeChanged | [ ] |
-
----
-
-## STATISTICS SUMMARY
-
-| Category | Total Items | Shared (S) | Divergent (D) | Editor Only (E) | Web Only (W) | Platform (P) |
-|----------|-------------|------------|---------------|------------------|--------------|--------------|
-| Atoms | 69 | 37 | 22 | 3 | 7 | 0 |
-| Molecules | 38 | 26 | 7 | 0 | 2 | 3 |
-| Organisms | 37 | 12 | 10 | 5 | 1 | 9 |
-| Features | 45 | 22 | 5 | 1 | 13 | 4 |
-| Templates | 11 | 8 | 2 | 1 | 0 | 0 |
-| Screens | 4 | 0 | 0 | 1 | 2 | 1 |
-| Hooks | 14 | 2 | 1 | 2 | 4 | 5 |
-| Store | 17 | 10 | 3 | 1 | 2 | 1 |
-| Utils | 38 | 10 | 8 | 8 | 11 | 1 |
-| Shared | 6 | 3 | 0 | 3 | 0 | 0 |
-| Services | 12 | 0 | 0 | 0 | 12 | 0 |
-| **TOTAL** | **291** | **130** | **58** | **25** | **54** | **24** |
diff --git a/docs/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md b/docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md
similarity index 100%
rename from docs/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md
rename to docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md
diff --git a/docs/HEADLESS_SETUP.md b/docs/outdated/HEADLESS_SETUP.md
similarity index 100%
rename from docs/HEADLESS_SETUP.md
rename to docs/outdated/HEADLESS_SETUP.md
diff --git a/docs/dead-code-inventory.md b/docs/outdated/dead-code-inventory.md
similarity index 100%
rename from docs/dead-code-inventory.md
rename to docs/outdated/dead-code-inventory.md
diff --git a/docs/debugger-opcua-shared-utilities.md b/docs/outdated/debugger-opcua-shared-utilities.md
similarity index 100%
rename from docs/debugger-opcua-shared-utilities.md
rename to docs/outdated/debugger-opcua-shared-utilities.md
diff --git a/docs/external-binaries-strategy.md b/docs/outdated/external-binaries-strategy.md
similarity index 100%
rename from docs/external-binaries-strategy.md
rename to docs/outdated/external-binaries-strategy.md
diff --git a/docs/name-type-linking-design.md b/docs/outdated/name-type-linking-design.md
similarity index 100%
rename from docs/name-type-linking-design.md
rename to docs/outdated/name-type-linking-design.md
diff --git a/docs/opcua-server-configuration/01-design-overview.md b/docs/outdated/opcua-server-configuration/01-design-overview.md
similarity index 100%
rename from docs/opcua-server-configuration/01-design-overview.md
rename to docs/outdated/opcua-server-configuration/01-design-overview.md
diff --git a/docs/opcua-server-configuration/02-ui-screen-specifications.md b/docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md
similarity index 100%
rename from docs/opcua-server-configuration/02-ui-screen-specifications.md
rename to docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md
diff --git a/docs/opcua-server-configuration/03-json-configuration-mapping.md b/docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md
similarity index 100%
rename from docs/opcua-server-configuration/03-json-configuration-mapping.md
rename to docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md
diff --git a/docs/opcua-server-configuration/04-implementation-phases.md b/docs/outdated/opcua-server-configuration/04-implementation-phases.md
similarity index 100%
rename from docs/opcua-server-configuration/04-implementation-phases.md
rename to docs/outdated/opcua-server-configuration/04-implementation-phases.md
diff --git a/docs/opcua-server-configuration/README.md b/docs/outdated/opcua-server-configuration/README.md
similarity index 100%
rename from docs/opcua-server-configuration/README.md
rename to docs/outdated/opcua-server-configuration/README.md
diff --git a/docs/s7comm-server-implementation.md b/docs/outdated/s7comm-server-implementation.md
similarity index 100%
rename from docs/s7comm-server-implementation.md
rename to docs/outdated/s7comm-server-implementation.md
diff --git a/docs/unified-frontend-serialization.md b/docs/outdated/unified-frontend-serialization.md
similarity index 100%
rename from docs/unified-frontend-serialization.md
rename to docs/outdated/unified-frontend-serialization.md
diff --git a/docs/variable-id-audit.md b/docs/outdated/variable-id-audit.md
similarity index 100%
rename from docs/variable-id-audit.md
rename to docs/outdated/variable-id-audit.md
diff --git a/docs/ports/WIRING.md b/docs/ports/WIRING.md
deleted file mode 100644
index c33381721..000000000
--- a/docs/ports/WIRING.md
+++ /dev/null
@@ -1,150 +0,0 @@
-# PlatformProvider Wiring Guide
-
-How to wire the PlatformProvider into each application root.
-
-## Strategy
-
-The migration lives in `src2/` — a parallel source directory that will eventually
-replace `src/` once all components are migrated and tested. The original `src/`
-remains untouched and fully functional throughout the migration.
-
-## Directory Structure
-
-```
-src/ <-- ORIGINAL, untouched during migration
- renderer/ (editor) or . (web)
- components/
- hooks/
- store/
- ...
-
-src2/ <-- MIGRATION target, new architecture
- providers/
- platform/
- index.ts <-- exports PlatformProvider, usePlatform, convenience hooks
- platform-context.tsx <-- React context + hooks (SHARED, identical in both repos)
- types.ts <-- PlatformPorts aggregate type (SHARED)
- ports/ <-- Port interfaces (SHARED, identical in both repos)
- types.ts
- compiler-port.ts
- runtime-port.ts
- debugger-port.ts
- simulator-port.ts
- project-port.ts
- device-port.ts
- system-port.ts
- window-port.ts
- accelerator-port.ts
- theme-port.ts
- platform-capabilities.ts
- index.ts
- adapters/
- editor-platform.ts <-- (editor only) Electron IPC implementations
- web-platform.ts <-- (web only) HTTP/WebRTC implementations
- components/ <-- migrated components (added over time)
- hooks/ <-- migrated hooks (added over time)
- store/ <-- migrated store slices (added over time)
-```
-
-## Editor Wiring (src2 App root)
-
-```tsx
-import { PlatformProvider } from '@src2/providers/platform'
-import { editorPorts } from '@src2/adapters/editor-platform'
-
-export default function App() {
- const { project: { meta: { path } } } = useOpenPLCStore()
-
- return (
-
-
- {path === '' ? : }
-
-
- )
-}
-```
-
-## Web Wiring (src2 App root)
-
-```tsx
-import { PlatformProvider } from '@src2/providers/platform'
-import { webPorts } from '@src2/adapters/web-platform'
-
-createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
-```
-
-## Using Ports in Components
-
-```tsx
-import { usePlatform, useCapabilities, useRuntime } from '@src2/providers/platform'
-
-// Full access to all ports
-function WorkspaceActivityBar() {
- const { compiler, runtime, debugger: dbg, capabilities } = usePlatform()
-
- const handleCompile = async () => {
- await compiler.compileProgram(args, (event) => {
- console.log(event.stage, event.message)
- })
- }
-
- return (
- <>
-
- {capabilities.hasNativeWindowControls && }
- {capabilities.hasAIAssistant && }
- >
- )
-}
-
-// Single port access
-function RuntimeLoginModal() {
- const runtime = useRuntime()
-
- const handleLogin = async (username: string, password: string) => {
- const result = await runtime.login({ username, password })
- if (result.success) { /* ... */ }
- }
-}
-
-// Feature toggle
-function TitleBar() {
- const caps = useCapabilities()
-
- return (
-
- {caps.hasNativeMenu ? : }
- {caps.hasNativeWindowControls && }
-
- )
-}
-```
-
-## Migration Workflow
-
-1. Pick a component from `src/` that calls `window.bridge.*` or a service directly
-2. Identify which port(s) it needs (e.g., RuntimePort for login)
-3. Implement the adapter method in `editor-platform.ts` / `web-platform.ts`
-4. Copy the component to `src2/`, refactoring it to use `usePlatform()` hooks
-5. Verify the migrated component works identically to the original
-6. Mark the item as migrated in `migration-tracker.md`
-
-The original `src/` component remains untouched until `src2/` is fully tested.
-Each stub port uses a Proxy that throws a descriptive error when called,
-so you'll immediately know if a component uses an unmigrated port.
-
-## Cutover
-
-Once all components are migrated and tested in `src2/`:
-1. Rename `src/` to `src-legacy/` (keep as backup)
-2. Rename `src2/` to `src/`
-3. Update build configs and path aliases
-4. Run full test suite
-5. Remove `src-legacy/` once confident
diff --git a/scripts/compare-dependencies.py b/scripts/compare-dependencies.py
new file mode 100644
index 000000000..4fc9566e6
--- /dev/null
+++ b/scripts/compare-dependencies.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+"""
+Compare shared production dependencies between openplc-web and openplc-editor.
+
+Reads package.json from both repos and checks that every dependency present
+in both has the same version specifier.
+Exit code 0 = all match, 1 = mismatches found.
+"""
+
+import argparse
+import json
+import sys
+from pathlib import Path
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Compare shared production dependency versions between repos."
+ )
+ parser.add_argument(
+ "--web-root",
+ required=True,
+ type=Path,
+ help="Path to the web repo root (containing package.json)",
+ )
+ parser.add_argument(
+ "--editor-root",
+ required=True,
+ type=Path,
+ help="Path to the editor repo root (containing package.json)",
+ )
+ args = parser.parse_args()
+
+ with open(args.web_root / "package.json") as f:
+ web_deps = json.load(f).get("dependencies", {})
+ with open(args.editor_root / "package.json") as f:
+ editor_deps = json.load(f).get("dependencies", {})
+
+ shared = sorted(set(web_deps) & set(editor_deps))
+ mismatches = []
+
+ for pkg in shared:
+ if web_deps[pkg] != editor_deps[pkg]:
+ mismatches.append((pkg, web_deps[pkg], editor_deps[pkg]))
+
+ if mismatches:
+ print(f"::error::Found {len(mismatches)} shared dependency version mismatch(es):")
+ for pkg, web_ver, editor_ver in mismatches:
+ print(f" {pkg}: web={web_ver} editor={editor_ver}")
+ return 1
+
+ print(f"All {len(shared)} shared dependencies match.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/compare-surface-definitions.py b/scripts/compare-surface-definitions.py
new file mode 100644
index 000000000..a8a2079d5
--- /dev/null
+++ b/scripts/compare-surface-definitions.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""
+Compare SURFACES definitions between the web and editor compare-surfaces.py scripts.
+
+Parses the SURFACES variable from both scripts using Python's AST module
+and checks that they define the same set of surfaces.
+Exit code 0 = match, 1 = mismatch or parse failure.
+"""
+
+from __future__ import annotations
+
+import argparse
+import ast
+import sys
+from pathlib import Path
+
+SCRIPT_RELATIVE_PATH = "scripts/compare-surfaces.py"
+
+
+def extract_surfaces(filepath: Path) -> list[str] | None:
+ with open(filepath) as f:
+ tree = ast.parse(f.read())
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Assign):
+ for target in node.targets:
+ if isinstance(target, ast.Name) and target.id == "SURFACES":
+ return sorted(ast.literal_eval(node.value))
+ return None
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Compare SURFACES definitions between repo compare-surfaces.py scripts."
+ )
+ parser.add_argument(
+ "--web-root",
+ required=True,
+ type=Path,
+ help="Path to the web repo root",
+ )
+ parser.add_argument(
+ "--editor-root",
+ required=True,
+ type=Path,
+ help="Path to the editor repo root",
+ )
+ args = parser.parse_args()
+
+ web_script = args.web_root / SCRIPT_RELATIVE_PATH
+ editor_script = args.editor_root / SCRIPT_RELATIVE_PATH
+
+ web = extract_surfaces(web_script)
+ editor = extract_surfaces(editor_script)
+
+ if web is None or editor is None:
+ missing = []
+ if web is None:
+ missing.append(f"web ({web_script})")
+ if editor is None:
+ missing.append(f"editor ({editor_script})")
+ print(f"::error::Could not extract SURFACES from: {', '.join(missing)}")
+ return 1
+
+ if web != editor:
+ web_only = set(web) - set(editor)
+ editor_only = set(editor) - set(web)
+ print("::error::SURFACES definitions do not match:")
+ if web_only:
+ print(f" Only in web: {', '.join(sorted(web_only))}")
+ if editor_only:
+ print(f" Only in editor: {', '.join(sorted(editor_only))}")
+ return 1
+
+ print(f"SURFACES definitions match: {web}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/compare-tooling.py b/scripts/compare-tooling.py
new file mode 100644
index 000000000..a48f5858f
--- /dev/null
+++ b/scripts/compare-tooling.py
@@ -0,0 +1,281 @@
+#!/usr/bin/env python3
+"""
+Compare tooling configuration between openplc-web and openplc-editor.
+
+Checks that lint, formatting, and TypeScript configurations produce
+equivalent output on shared surfaces. Byte-identical files are hash-
+compared; config files with legitimate platform differences are
+structurally compared.
+
+Exit code 0 = all in sync, 1 = differences found.
+"""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import re
+import sys
+from pathlib import Path
+
+# ---------------------------------------------------------------------------
+# Configuration
+# ---------------------------------------------------------------------------
+
+# Files that must be byte-identical (same relative path in both repos)
+IDENTICAL_FILES = [
+ ".prettierrc",
+ "scripts/compare-surfaces.py",
+ "scripts/compare-dependencies.py",
+ "scripts/compare-surface-definitions.py",
+ "scripts/compare-tooling.py",
+]
+
+# ESLint config (different filenames, rules must match)
+ESLINT_FILES = {
+ "web": "eslint.config.js",
+ "editor": "eslint.config.mjs",
+}
+
+# TypeScript config (different structure, key options must match)
+TSCONFIG_FILES = {
+ "web": "tsconfig.app.json",
+ "editor": "tsconfig.json",
+}
+
+# Compiler options that must match for shared surface type-checking consistency
+TSCONFIG_SHARED_OPTIONS = [
+ "strict",
+ "jsx",
+ "noFallthroughCasesInSwitch",
+ "noUnusedLocals",
+ "noUnusedParameters",
+ "paths",
+]
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+def hash_file(path: Path) -> str:
+ h = hashlib.sha256()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(8192), b""):
+ h.update(chunk)
+ return h.hexdigest()
+
+
+def strip_eslint_ignores(text: str) -> str:
+ """Remove the { ignores: [...] } config block from ESLint config text."""
+ return re.sub(
+ r"\{\s*ignores:\s*\[.*?\],?\s*\}",
+ "",
+ text,
+ flags=re.DOTALL,
+ )
+
+
+def normalize_whitespace(text: str) -> str:
+ """Collapse runs of whitespace and strip for comparison."""
+ return re.sub(r"\s+", " ", text).strip()
+
+
+def parse_jsonc(text: str) -> dict:
+ """Parse JSON with comments (// and /* */) and trailing commas."""
+ # Strip comments while preserving strings containing //
+ result = []
+ i = 0
+ while i < len(text):
+ if text[i] == '"':
+ j = i + 1
+ while j < len(text):
+ if text[j] == '\\':
+ j += 2
+ continue
+ if text[j] == '"':
+ j += 1
+ break
+ j += 1
+ result.append(text[i:j])
+ i = j
+ elif text[i:i+2] == '//':
+ i = text.index('\n', i) if '\n' in text[i:] else len(text)
+ elif text[i:i+2] == '/*':
+ end = text.index('*/', i)
+ i = end + 2
+ else:
+ result.append(text[i])
+ i += 1
+ text = "".join(result)
+ # Strip trailing commas before } or ]
+ text = re.sub(r",\s*([}\]])", r"\1", text)
+ return json.loads(text)
+
+
+# ---------------------------------------------------------------------------
+# Checks
+# ---------------------------------------------------------------------------
+
+def check_identical_files(
+ web_root: Path, editor_root: Path
+) -> list[dict]:
+ results = []
+ for relpath in IDENTICAL_FILES:
+ web_file = web_root / relpath
+ editor_file = editor_root / relpath
+
+ entry: dict = {"file": relpath, "check": "byte-identical"}
+
+ if not web_file.exists() and not editor_file.exists():
+ entry["status"] = "skip"
+ entry["reason"] = "missing in both repos"
+ elif not web_file.exists():
+ entry["status"] = "fail"
+ entry["reason"] = "missing in web repo"
+ elif not editor_file.exists():
+ entry["status"] = "fail"
+ entry["reason"] = "missing in editor repo"
+ elif hash_file(web_file) == hash_file(editor_file):
+ entry["status"] = "pass"
+ else:
+ entry["status"] = "fail"
+ entry["reason"] = "files differ"
+
+ results.append(entry)
+ return results
+
+
+def check_eslint_rules(
+ web_root: Path, editor_root: Path
+) -> list[dict]:
+ web_file = web_root / ESLINT_FILES["web"]
+ editor_file = editor_root / ESLINT_FILES["editor"]
+
+ entry: dict = {
+ "file": f"{ESLINT_FILES['web']} <-> {ESLINT_FILES['editor']}",
+ "check": "eslint-rules",
+ }
+
+ if not web_file.exists() or not editor_file.exists():
+ missing = []
+ if not web_file.exists():
+ missing.append(f"web ({web_file})")
+ if not editor_file.exists():
+ missing.append(f"editor ({editor_file})")
+ entry["status"] = "fail"
+ entry["reason"] = f"missing: {', '.join(missing)}"
+ return [entry]
+
+ web_text = web_file.read_text()
+ editor_text = editor_file.read_text()
+
+ web_stripped = normalize_whitespace(strip_eslint_ignores(web_text))
+ editor_stripped = normalize_whitespace(strip_eslint_ignores(editor_text))
+
+ if web_stripped == editor_stripped:
+ entry["status"] = "pass"
+ else:
+ entry["status"] = "fail"
+ entry["reason"] = "ESLint rules/plugins/config differ (ignoring ignores block)"
+
+ return [entry]
+
+
+def check_tsconfig_options(
+ web_root: Path, editor_root: Path
+) -> list[dict]:
+ web_file = web_root / TSCONFIG_FILES["web"]
+ editor_file = editor_root / TSCONFIG_FILES["editor"]
+
+ entry: dict = {
+ "file": f"{TSCONFIG_FILES['web']} <-> {TSCONFIG_FILES['editor']}",
+ "check": "tsconfig-shared-options",
+ }
+
+ if not web_file.exists() or not editor_file.exists():
+ missing = []
+ if not web_file.exists():
+ missing.append(f"web ({web_file})")
+ if not editor_file.exists():
+ missing.append(f"editor ({editor_file})")
+ entry["status"] = "fail"
+ entry["reason"] = f"missing: {', '.join(missing)}"
+ return [entry]
+
+ with open(web_file) as f:
+ web_config = parse_jsonc(f.read())
+ with open(editor_file) as f:
+ editor_config = parse_jsonc(f.read())
+
+ web_opts = web_config.get("compilerOptions", {})
+ editor_opts = editor_config.get("compilerOptions", {})
+
+ mismatches = []
+ for opt in TSCONFIG_SHARED_OPTIONS:
+ web_val = web_opts.get(opt)
+ editor_val = editor_opts.get(opt)
+ if web_val != editor_val:
+ mismatches.append(
+ f"{opt}: web={json.dumps(web_val)} editor={json.dumps(editor_val)}"
+ )
+
+ if mismatches:
+ entry["status"] = "fail"
+ entry["reason"] = "; ".join(mismatches)
+ else:
+ entry["status"] = "pass"
+
+ return [entry]
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Compare tooling configuration between web and editor repos."
+ )
+ parser.add_argument(
+ "--web-root", required=True, type=Path,
+ help="Path to the web repo root",
+ )
+ parser.add_argument(
+ "--editor-root", required=True, type=Path,
+ help="Path to the editor repo root",
+ )
+ parser.add_argument(
+ "--github-annotations", action="store_true",
+ help="Emit GitHub Actions error annotations",
+ )
+ args = parser.parse_args()
+
+ all_results: list[dict] = []
+ all_results.extend(check_identical_files(args.web_root, args.editor_root))
+ all_results.extend(check_eslint_rules(args.web_root, args.editor_root))
+ all_results.extend(check_tsconfig_options(args.web_root, args.editor_root))
+
+ failures = [r for r in all_results if r["status"] == "fail"]
+ passes = [r for r in all_results if r["status"] == "pass"]
+ skips = [r for r in all_results if r["status"] == "skip"]
+
+ for r in passes:
+ print(f" PASS {r['file']} ({r['check']})")
+ for r in skips:
+ print(f" SKIP {r['file']} -- {r.get('reason', '')}")
+ for r in failures:
+ msg = f" FAIL {r['file']} ({r['check']}): {r.get('reason', '')}"
+ print(msg)
+ if args.github_annotations:
+ print(f"::error::{r['file']}: {r.get('reason', 'mismatch')}", file=sys.stderr)
+
+ print()
+ print(f"{len(passes)} passed, {len(failures)} failed, {len(skips)} skipped")
+
+ return 1 if failures else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tsconfig.json b/tsconfig.json
index ab5adf630..31900d6d4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,6 +7,9 @@
"lib": ["dom", "es2022"],
"jsx": "react-jsx",
"strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"sourceMap": true,
"moduleResolution": "node",