Skip to content

v4.2.0-rc1 — STruC++ compiler + Vendor Plugin Packages#748

Open
thiagoralves wants to merge 243 commits into
developmentfrom
v4.2.0-rc1
Open

v4.2.0-rc1 — STruC++ compiler + Vendor Plugin Packages#748
thiagoralves wants to merge 243 commits into
developmentfrom
v4.2.0-rc1

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

@thiagoralves thiagoralves commented May 5, 2026

Summary

This release reconciles two long-running feature branches into a single line targeting v4.2.0:

  • STruC++ compiler replaces MatIEC across the IEC compile pipeline.
  • Vendor Plugin Packages (VPP) — custom hardware boards now install as .vpp packages; the editor ships with only OpenPLC Simulator + Runtime v3 + Runtime v4 by default.

116 non-merge commits, 383 files. The +139k lines line stat is dominated by the bundled avr-libstdcpp (199 freestanding-libstdc++ headers needed so STruC++ programs compile against the AVR Mega2560 simulator toolchain).

Headline features

STruC++ compiler integration

The whole compile pipeline rebuilt around STruC++:

  • compiler-module.ts invokes the STruC++ TypeScript API instead of the MatIEC binary chain. Generated C++ is split per-POU (one TU per FB / Program / Function plus a shared configuration.cpp) so on-target builds can make -j$(nproc) and ccache reuses unchanged .o files.
  • Runtime headers + bundled .stlib archives ship under resources/strucpp/ and node_modules/strucpp/.
  • avr-libstdcpp shim under resources/sources/avr-libstdcpp/include/ provides <cstdint>, <type_traits>, <array>, <string>, <initializer_list>, etc. for AVR builds — Arduino's avr-gcc ships no libstdc++. Compiler-module wires the include path when core starts with arduino:avr.
  • gcc-style diagnostics: source line + caret pointer rendered in the editor console.
  • Per-task scan-cycle stats: STATS endpoint returns tasks: [{ name, scan_count, scan_time_*, cycle_time_*, cycle_latency_*, overruns }]. Editor renders one row per task in the table.
  • Real IEC task names propagate from the project file through STruC++ codegen into the runtime — no more synthetic plc-task-N placeholders.
  • Soft-write through the new debugger surface — OPC-UA / monitor / debugger can push values to variables without latching them as forces.

Vendor Plugin Packages (VPP)

  • New vendor screen + package manager UI (under src/frontend/components/_features/[workspace]/editor/device/... and src/frontend/screens/). Browse, install, uninstall .vpp packages.
  • New PackagePort middleware port with editor adapter implementation. IPC bridge handlers (packageManager:*).
  • Board catalog (hals.json) trimmed to OpenPLC-only defaults — VPP boards are dynamically registered after install.
  • Vendor-specific IO mapping: variables can target VPP-contributed locations alongside the standard digital/analog pin model. Address allocation is unified with EtherCAT in src/backend/shared/utils/iec-address/.
  • Editor sends START after a successful build (replaces runtime's auto-start), with COMMAND:BUSY retry handling.
  • Plugin-contributed stats panel: any VPP plugin that exports get_stats shows up as its own card grid below the IEC + EtherCAT stats. Rendered identically on the device-board screen (Electron) and the orchestrators screen (web) via a shared PluginStatsPanel molecule.
  • Communication / Modbus-RTU sections removed from the device editor (Arduino-only paths now belong to the relevant VPP package).

EtherCAT (already on dev, refined here)

  • Dedicated SCHED_FIFO bus thread with split-mutex IO model.
  • Period / wake-up latency stats alongside bus-cycle duration; EWMA averaging targeted to a 2 s wall-clock window so the polled value stays stable.
  • Standalone EtherCATStats table component, rendered on both board + orchestrators screens.
  • iface isolation reduced to NIC tuning (ethtool coalescing + offloads + SO_BUSY_POLL) — iptables / IPv6 sysctl manipulation removed (was bricking single-NIC Pis).

UI polish

  • Stats panels rebuilt as compact tables (drop the older card grid).
  • Workspace activity bar gets package-manager + version-control entries.
  • OPC-UA tabs aligned with the S7Comm design system.
  • AI chat panel viewport overflow fix.
  • Select-dropdown scrollbar made always-visible (Radix's default hide-on-idle was a usability problem in long board lists).

Breaking changes

  • Project schema: OpcUaNodeConfig.initialValue removed; PLCTask.isSystemTask and PLCTask.associatedDevice removed (the synthetic EtherCAT system-task they backed never shipped). Project files mid-flight on the strucpp branch may need re-saving.
  • Runtime ABI: requires the matching v4.2.0-rc1 runtime — the debugger surface and plugin args struct changed end-to-end. See the runtime PR.
  • Board catalog: ships with three OpenPLC entries by default. Re-installing previously-shipped Arduino boards now goes through VPP.

File scope highlights

  • src/backend/editor/compiler/compiler-module.ts — full rewrite around STruC++
  • src/backend/editor/package-manager/ — new module
  • src/backend/shared/utils/iec-address/ — unified IEC-address collector
  • src/backend/shared/utils/vpp/ — VPP plugin config generator
  • src/frontend/components/_features/[workspace]/editor/device/{ethercat,vendor-screen,package-manager}/ — new UI
  • src/frontend/components/_molecules/{scan-cycle-stats,ethercat-stats,plugin-stats-panel}/ — table-shape stats components
  • src/frontend/store/slices/{vpp,version-control,navigation,...}/ — store extensions
  • src/middleware/shared/ports/{package-port,version-control-port,navigation-port,types}.ts — port additions
  • resources/sources/avr-libstdcpp/include/ — bundled freestanding libstdc++ for AVR (~199 files)
  • resources/strucpp/ — STruC++ runtime headers + bundled stlibs

Test plan

  • Simulator: compile + run the Irrigation Controller (or any non-trivial project) on OpenPLC Simulator. Cycle stats appear, force/unforce/soft-write all work.
  • Pi 4 with EtherCAT: connect to runtime, start program, EtherCAT stats panel populates, plugin_stats card grid appears.
  • VPP install: install a .vpp package built from openplc-packages, see the new board in the selector.
  • VPP project: build a project that includes VPP-contributed IO; on-target plugin compile completes; plugin's get_stats output shows in the editor.
  • gcc-style diagnostics: introduce a syntax error in ST source, confirm the source line + caret render in the editor console.
  • Web build (orchestrators screen): stats panels render identical to Electron board screen.
  • Tests: npm test (3290 pass), npm run validate:arch (clean), TS strict (clean).

After merge

Delete feat/vpp-package-support-v2 and strucpp-implementation — their work is fully captured here. Future feature branches start from development per usual flow.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Editor/compiler migration to STruC++; debugger moved to a hierarchical per-leaf model with new debug APIs and updated runtime/task threading model.
  • Documentation
    • Large STruC++ migration series and a comprehensive debugger scalability/architecture analysis with phased rollout and plans.
  • Chores
    • Updated binary listings and ignore rules, added extract-zip dependency, packaged StrucPP runtime assets, enabled JSON in editor build, and minor test/config updates.

Review Change Stack

thiagoralves and others added 30 commits April 10, 2026 10:07
Add PackageManagerModule for importing, listing, and uninstalling VPP
packages. Extend HardwareModule to merge VPP boards into available boards
list and support package-relative preview images. Add VPP fallback to
CompilerModule's board runtime resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add VPP domain types (VppMetadata, PackageManifest, InstalledPackage,
IoMappingEntry, etc.) to shared port types. Create PackagePort interface
for platform-agnostic package management. Create editor package adapter
that delegates to IPC bridge. Extend DevicePort.getPreviewImage with
optional packagePath. Add hasPackageManager capability flag. Wire
packages port into editor platform.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add package manager IPC handlers in main process for import, list,
uninstall, and manifest operations. Add corresponding renderer bridge
methods. Update getPreviewImage to support optional packagePath for
VPP boards. Add "Board Package Manager..." menu item to both Darwin
and default menu templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add vendorScreenData field to device configuration schema. Add
setVendorScreenData action to device slice. Add plc-vendor-screen
and plc-package-manager editor model variants. Add vendor-screen
and package-manager tab element types with creation helpers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add frontend utility for collecting used IEC addresses across all IO
sources (pins, remote devices, vendor modules) and generating next
available addresses. Pure frontend code with no backend dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create VendorScreenRenderer with section renderer and layout components
(module-slots, io-table, form, unsupported). Create PackageManagerEditor
using usePlatform() hooks instead of direct IPC calls. Create
VendorScreenEditor wrapper. All components use platform-agnostic
imports for cross-platform reuse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add vendor-screen and package-manager tab icons and type handling.
Add vendor screen leaves to project tree when VPP board is selected.
Route plc-vendor-screen and plc-package-manager editor types in
workspace screen. Subscribe to package manager open/boards-updated
events via packages port. Pass packagePath for VPP board previews.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Add "Install additional boards..." option at the end of the Device
   dropdown that opens the Package Manager tab.
2. Replace emoji icons in debugger-message modal with the existing
   WarningIcon SVG component.
3. Use screen names as-is from the manifest instead of applying
   camelCase splitting (they are already human-readable).
4. Add vendor IO mapping aliases to the POU variable location dropdown
   by creating buildVendorIoOptionGroups utility and vendorIoSelectors
   hook, integrated into both variables-table and global-variables-table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove separator and dark mode color override to match the original
VPP branch styling for the install boards dropdown option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore the border-t separator before the install option matching the
original branch, and add a + prefix to the label text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detailed investigation of why compilation fails with large PLC projects
(32K+ debug variables). Documents the root causes across the full pipeline
(xml2st, MatIEC, runtime, editor) and proposes 6 fix strategies ranging
from quick platform-aware limits to a full dynamic registration architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/types/PLC/devices/configuration.ts
Comprehensive 8-phase migration plan covering the replacement of iec2c
(MatIEC) with STruC++ for ST-to-C++ compilation, redesigning the
debugger for scalability (hierarchical variable addressing instead of
flat array expansion), and adapting both Arduino and Runtime v4 backends.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace fragile file:../strucpp approach with the same download mechanism
used for matiec and xml2st: version tracked in binary-versions.json,
downloaded from GitHub Releases via scripts/download-binaries.ts.

Runtime headers come from the same release artifact as the compiler,
ensuring strict version coupling. No local copies stored in the repo.

Also documents required changes to STruC++ release workflow (add npm
pack step to produce .tgz artifact).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add strucpp v0.2.4 entry to binary-versions.json
- Extend scripts/download-binaries.ts to download the STruC++ npm
  tarball from GitHub Releases, install it into node_modules, and
  extract runtime headers + .stlib libraries to resources/strucpp/
- Add resources/strucpp/ to .gitignore
- Update Phase 1 docs to reflect simplified approach: no compile
  wrapper needed, editor calls compile() directly, Arduino runtime
  navigates STruC++ structures dynamically

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove all references to the TypeScript glue code generator and compile
wrapper. The new design is simpler:

- Phase 2: Arduino sketch is fully static C++ that navigates STruC++
  structures dynamically (locatedVars[], ConfigurationInstance, etc.)
- Phase 4: compiler-module.ts calls compile() directly, no wrapper
- Phase 6: v4_compat.cpp is a static shim, same philosophy

Configuration name is always Config0 (hardcoded by OpenPLC), so the
sketch can instantiate Configuration_Config0 directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorder phases so the editor compiler pipeline (code generation) comes
before the Arduino runtime (code consumption). This lets us validate
STruC++ output before building the runtime against it.

New phase order:
  Phase 1: STruC++ dependency infrastructure (done)
  Phase 2: Editor compiler pipeline (wire compile() into compiler-module)
  Phase 3: Arduino runtime adaptation (sketch + openplc.h)
  Phase 4: Debugger (deferred)
  Phase 5-7: Runtime v4 (renumbered from 6-8)

Also fix download-binaries.ts to use --no-save when installing strucpp
to avoid polluting package.json with temp paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update all phases to reflect that this branch fully replaces MatIEC
with no backward compatibility:

- Phase 2: Remove dual pipeline routing and compiler_backend field.
  MatIEC pipeline is deleted, not preserved alongside STruC++.
- Phase 3: openplc.h MatIEC declarations are removed, not guarded.
- Phase 4: Debugger designed from clean slate, no old format support.
- Phase 5: compile.sh has no MatIEC fallback branch.
- Phase 6: No legacy single-thread model for MatIEC .so files.
- Phase 7: No flat-index backward compatibility layer.
- Overview: Replace "Dual Pipeline Coexistence" with "Full Replacement".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the iec2c binary invocation with STruC++ compile() for
ST-to-C++ compilation. This is a clean break -- no dual pipeline,
no backward compatibility with MatIEC.

Changes:
- compiler-module.ts: Remove handleTranspileSTtoC (iec2c),
  handleGenerateDebugFiles, handleGenerateGlueVars,
  handlePatchGeneratedFiles, checkIec2cAvailability. Add
  handleCompileSTtoCpp using STruC++ compile(). Update
  copyStaticFiles to copy STruC++ runtime headers. MD5 hash
  computed directly from program.st content.
- binary-versions.json: Remove matiec entry
- download-binaries.ts: Remove matiec download logic
- compiler-adapter.ts: Update inferStage for STruC++ messages
- jest.config.json: Add transformIgnorePatterns for strucpp ESM
- Tests updated: all 37 tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add hals.json cxx_flags update and end-to-end compilation test to
Phase 3 docs, since both require the Arduino sketch to exist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Arduino sketch is a Phase 3 artifact. The pipeline should not fail
at the static file copy step -- it should proceed through STruC++
compilation and only fail at arduino-cli (which requires the sketch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace MatIEC's {{ ... }} embedded C syntax with STruC++'s
{external ... } pragma in both Python and C++ POU code generators.

STruC++ uses {external ... } to pass C/C++ code through AS-IS to the
generated output. The content inside is identical -- only the delimiter
syntax changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create StrucppBaremetal.ino: static sketch that dynamically navigates
  STruC++ runtime structures -- walks locatedVars[] for I/O binding,
  discovers tasks via Configuration/Resource/Task, computes GCD for
  scan cycle timing, schedules programs with per-task divisors.
- Clean up openplc.h: remove MatIEC-specific declarations (config_init__,
  config_run__, glueVars, updateTime, common_ticktime__). Keep IEC types,
  buffer pointers, buffer size macros, and HAL functions.
- Add -std=gnu++17 to cxx_flags for all 62 boards in hals.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.2.5 adds iec_platform.hpp shim so runtime headers use <stdint.h>
instead of <cstdint> on AVR targets where C++ standard library wrappers
are not available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AVR GCC doesn't ship C++ standard library headers (<type_traits>,
<algorithm>, <cstdint>, etc.) that STruC++ runtime headers require.
Bundle avr-libstdcpp (GPLv3, header-only, from modm-io/avr-libstdcpp)
and add -isystem include path for AVR boards during compilation.

STruC++ runtime headers stay standard-compliant -- the AVR compatibility
is handled entirely at the editor build pipeline level.

Also bump strucpp to v0.2.6 (reverted AVR-specific changes from runtime).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.2.7 guards <stdexcept> includes and throw statements with
#ifndef __AVR__ so runtime headers compile on AVR targets where
exceptions are not available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The full avr-libstdcpp (GCC 10) is incompatible with Arduino's AVR GCC
7.3 due to c++config.h version mismatches. Replace with minimal custom
headers that provide only what STruC++ runtime needs:

- type_traits: integral_constant, enable_if, is_same, is_integral,
  is_floating_point, is_arithmetic, is_signed, is_unsigned, is_enum,
  is_class, is_base_of, is_convertible, underlying_type, etc.
- utility: move, forward, swap
- algorithm: min, max, find, copy, fill
- array: std::array<T,N>
- C wrappers: cstdint, cstddef, cstring, cstdlib, cmath, cstdio
- Stubs: string, stdexcept, ostream, new (for headers that include
  them but guard usage with #ifndef __AVR__)

Also fix: use -I (not -isystem) for AVR C++ headers -- GCC 7.3
treats -isystem headers as C linkage on AVR, causing "template with
C linkage" errors.

Also fix: rename StrucppBaremetal.ino to Baremetal.ino (Arduino
requires .ino filename to match directory name).

Also fix: #undef min/max in sketch to prevent Arduino macro conflicts
with std::min/std::max and numeric_limits.

Remaining issues to fix:
- iec_located.hpp static_assert for 16-byte LocatedVar fails on AVR
  (pointers are 2 bytes, not 8)
- Arduino binary.h defines B0-B7 macros conflicting with template
  params in iec_traits.hpp
- iec_array.hpp uses C++17 deduction guides not supported by GCC 7.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Editor-side fixes for successful Arduino Mega compilation:

Baremetal.ino:
- #undef Arduino macros (min/max/abs/round/TIMER*) before STruC++ includes
- Include openplc.h before generated.hpp to avoid IEC type ambiguity
- Use ::IEC_BOOL etc. (global scope) in casts to disambiguate from strucpp::
- Configuration_CONFIG0 (STruC++ uppercases configuration names)
- Define I/O buffer arrays (previously in MatIEC glueVars.c)
- Provide sized operator delete stub (AVR virtual destructors need it)
- Define __CURRENT_TIME_NS global for STruC++ time operations
- Renamed to Baremetal.ino (Arduino requires filename = directory name)

openplc.h:
- Wrap HAL functions in extern "C" for proper C/C++ linkage

avr-libstdcpp:
- Add make_unsigned/make_signed, common_type to type_traits
- Add std::nullptr_t to cstddef
- Add strtoll/strtoull stubs to cstdlib (AVR libc lacks 64-bit strtol)
- Add missing C functions to cstring and cstdlib
- Add abs/trunc/log10 overloads to cmath

compiler-module.ts:
- Use -I (not -isystem) for avr-libstdcpp (GCC 7.3 treats -isystem as C linkage)

Bump strucpp to v0.2.8 (AVR runtime header compatibility).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
thiagoralves and others added 7 commits May 14, 2026 22:06
The runtime now loads VPP plugins exclusively from vpp_plugins.conf,
which the editor owns and generates on every program upload when the
target is a VPP runtime-v4 board.

The file lists one entry per VPP plugin in the same CSV format as
plugins.conf (name, .so path, enabled, type, config_path, venv_path),
with deterministic paths that match what compile.sh produces and
apply_vpp_plugin_conf() copies the config to:

  <name>,./build/vpp/lib<name>_plugin.so,1,1,./build/vpp/<name>.json,

For non-VPP uploads (vanilla Runtime v4 targets), no vpp_plugins.conf
is generated, so apply_vpp_plugin_conf() on the runtime side will
delete any stale file, ensuring clean program-switch behaviour.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The compiler module looks for STruC++ runtime headers at
<resourcesPath>/strucpp/runtime/include/ in packaged mode, but the
electron-builder config only shipped resources/bin/ and
resources/sources/ as extraResources. The strucpp directory
(populated by scripts/download-binaries.ts during postinstall) was
never copied into the installed app, so any compile attempt failed
with:

  STruC++ runtime headers not found at .../resources/strucpp/runtime/include

Adds an extraResources entry per platform (mac/win/linux) mapping
./resources/strucpp -> ./strucpp, mirroring the bin and sources
entries that were already correct.

Note: the cosmetic "STruC++ available at version 0.0.0" log line is
a separate strucpp-side issue — its getVersion() falls back to
"0.0.0" when it can't read its own package.json at runtime, which
happens because the package is bundled inside the editor's webpack
output (and thus inside an asar archive) where the relative
package.json lookup doesn't resolve. The module itself is loaded
and functional; only the version string is wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the version-baking fix so getVersion() reports the real
version (instead of "0.0.0") when consumed from inside the editor's
webpack + asar bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(vpp): backplane editor + per-slot module config
…e include

The HAL templates included board defines as
  #include "../examples/Baremetal/defines.h"
relative to arduino.cpp in the build's src/ folder. That works
locally when avr-g++ resolves the include from the source file's
on-disk location, but it breaks on:

  - Paths with spaces inside unquoted -I/-include argv (Windows
    Parallels shared-folder mounts like C:\Mac\Home\... that surface
    "PLC Progs", "OpenPLC Simulator" etc. in the build path).
  - Any arduino-cli flow that copies arduino.cpp into its temp
    sketch sandbox before preprocessing, where "../examples/..."
    resolves to a sibling of the sandbox instead of the project.

The user-reported failure on a fresh Windows VM was the second
mode: arduino-cli failed with "fatal error: ../examples/Baremetal/
defines.h: No such file or directory" even though the file existed
at the correct project-relative path.

Fix: drop the directory-relative include entirely.

  - All 29 HAL templates now `#include "defines.h"` directly.
  - The editor writes defines.h to <build>/<board>/src/defines.h,
    next to arduino.cpp.

arduino.cpp finds defines.h in the same directory. Sketch files
(Baremetal.ino, ModbusSlave.h) already use `#include "defines.h"`
and resolve it via the `-I src/` library include arduino-cli adds
when it recognises src/ as a library — same as before. No other
path adjustments needed.

The `examples/Baremetal/` directory is still produced (sketch +
hex output) but no longer hosts defines.h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cate

The FBD editor reads its render data exclusively from `fbdFlows`,
falling through to a `<span>No rung found for editor</span>` branch
when the lookup misses.  `pouActions.create` (and `.duplicate`) only
pushed the new POU into `project.data.pous` — never into the flow
slice — so a freshly-created FBD POU opened to that fallback string
instead of an empty canvas.  Ladder dodged the visible symptom
because its editor unconditionally renders a "Create Rung" button
when `rungs[]` is empty, but the missing flow entry was the same
underlying gap.

The new-project wizard works because `handleOpenProjectResponse`
iterates all loaded POUs and seeds both flow slices.  The "Add POU"
path bypassed that, which is the reported regression in #759.

Fix: after `createPou` succeeds in both `create` and `duplicate`,
push the body value into the matching flow slice.  For `duplicate`,
override the flow's `name` field because the shallow-copied body
still carries the source POU's name.

Fixes #759

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the ~33 development commits accumulated since v4.2.0-rc1 was
cut back onto the release branch: EtherCAT scan + stats pipeline,
AI chat-history persistence, ladder branches with nested parallels
(#756), and the server-tree visibility fix.

Substantive conflict resolutions:

* `store/slices/device/{types,slice}.ts` + tests
    Both branches added different actions to the device slice
    (rc1: vendor-screen persistence; dev: ethercat status + polling
    toggle + temporary DHCP IP). Kept both sets verbatim.

* `_features/.../device/configuration/board.tsx`
* `_features/.../device/orchestrators/orchestrators-list.tsx`
    Both branches reworked the connected-runtime stats panel. Kept
    rc1's `<ScanCycleStats>` / `<EtherCATStats>` / `<PluginStatsPanel>`
    molecule layout (renders immediately on connect, no scan-count
    gate) and adopted dev's `setIncludeEthercatStatsInPolling`
    polling toggle so the global `useRuntimePolling` hook fetches
    EtherCAT status only while a stats screen is mounted.

* `_molecules/ethercat-stats/index.tsx`
    Rewrote so the molecule consumes
    `runtimeConnection.ethercatStatus` from the device slice instead
    of self-fetching on its own 2 s timer. Extended the column set
    with the metrics dev's `EthercatStatsSection` introduced (Master
    State, Slave Count, Max Exchange, Recovery Attempts, WKC
    consecutive-error badge) so no new health info is lost moving
    from cards back to the table.

* `_features/.../device/ethercat/components/ethercat-stats-section.tsx`
    Deleted — its render is now handled by the unified
    `<EtherCATStats>` molecule.

* `_organisms/explorer/project.tsx`
    Combined both gating intents: kept rc1's
    `projectCaps.hasServers` library gate AND dropped the
    `capabilities.hasLocalSerialPorts` clause per dev's d257e2a
    ("show Servers branch on platforms without local serial ports").

* `_features/.../device/ethercat/index.tsx`
    Kept rc1's `collectUsedIecAddresses` import path
    (`backend/shared/utils/iec-address`) and the extra
    `vendorScreenData` argument it now takes; adopted dev's `Modal`
    imports and `unmatched`-devices tracking in
    `handleAddSelectedFromScan`.

Test-fixture updates to match the now-richer `EtherCATCycleMetrics`
shape (min_*, period_*, latency_* fields are mandatory) and the
expanded `RuntimeConnection` state (`ethercatStatus` +
`includeEthercatStatsInPolling`).

`npx tsc --noEmit`, `npm run build:main`, and the device / shared /
ethercat / ladder test suites (1061 tests) all pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thiagoralves and others added 18 commits May 19, 2026 07:42
VPP packages can now declare REAL-typed analog channels (e.g. the
SLM-THM-4 thermocouple module, whose wire format is an IEEE-754 float
per channel). Those channels need to land in the runtime's 32-bit
image table, not the 16-bit one, so the plugin config now carries
separate dword-range mappings.

generate-vendor-plugin-config:
  - Channels are bucketed by addressPrefix instead of type, so an
    "analogInput" with %ID and one with %IW route to different
    slot.io_mapping fields.
  - New DWORD_ADDRESS_REGEX + parseDwordAddress + buildDwordRange.
  - PluginSlotIoMapping gains analog_real_inputs / analog_real_outputs
    (base_dword + count).

Plain-INT (%IW/%QW) channels behave exactly as before; the new buckets
are only populated when the manifest declares %ID/%QD channels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VPP packages can now ship modules whose channel layout depends on a
config-screen value (e.g. SLM-RP4 V/mA cards flipping between raw
UINT/%IW and REAL/%ID for engineering units). The manifest declares
`formatFieldId` + `channelsByFormat: { <value>: [channels...] }` on
`addressMapping`; the editor picks the right array per slot.

src/frontend/utils/vpp/resolve-module-channels.ts (new):
  - Pure resolver that takes (moduleDef, slotConfig) and returns the
    channel array. Falls back to `channels` when no format mechanism
    is declared, or when channelsByFormat is missing the resolved key.
  - 6 unit tests cover the resolution paths.

io-table-layout.tsx, module-slots-layout.tsx:
  - Both layouts use the resolver and depend on a derived
    `formatSelectionKey` so re-allocation fires when the user toggles
    a slot's format selector (not only when the slot module list
    itself changes).
  - Modules without channelsByFormat fall back to the legacy `channels`
    array, so every existing VPP behaves exactly as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The variable-location validator only allowed memory prefixes (%MD,
%ML), so users mapping a REAL/DINT/LREAL to a VPP module's %ID/%QD
or %IL/%QL address hit "Location is invalid - valid locations: %MD0"
even though those prefixes are perfectly valid IEC 61131-3 addresses.

Widens DWORD_LOCATION_REGEX and LWORD_LOCATION_REGEX to accept any
of %[QIM]D / %[QIM]L. Updates the error message to list all three.
The auto-increment-on-collision logic now preserves the user's
prefix instead of forcing %MD/%ML, mirroring the existing pattern
already used for word-sized variables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a module uses channelsByFormat (e.g. SLM-AI4-AO2-V flipped to
engineering mode), the manifest's fallback `channels` array still
carries the raw-mode prefixes (%IW/%QW), but the editor allocated
%ID/%QD addresses in the io-mapping. The plugin-config generator was
bucketing channels by the manifest's static channel.addressPrefix,
so it dropped the channels into the wrong bucket and buildWordRange
failed to parse them as %IW — net effect: no analog_real_inputs /
analog_real_outputs block in the plugin config, the SLM plugin saw
ai_real_count = ao_real_count = 0, and the REAL path silently never
fired (AO writes had no effect, AI readbacks were stuck at 0).

Now bucket by the io-mapping's actual iecAddress prefix, which is
the authoritative resolved address. The manifest channel array is
still iterated for channel-name -> io-entry lookup, but only the
allocated address determines the bucket.

Side effect: a single malformed address no longer kills the entire
bucket — it gets silently dropped and the valid addresses are still
emitted (partial success rather than all-or-nothing). Updated the
related test to reflect the new behavior.

Adds a regression test that mirrors the bug exactly: a module
declaring %IW/%QW channels in the fallback array but with %ID/%QD
addresses in the io-mapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backplane Configuration screen gets two improvements that VPP
packages can opt into independently:

1. Selected-slot indicator. The slot row of the currently selected
   slot now picks up the brand-tinted background, a bold label, and
   a 3px inset left-border (the same pattern Library Manager uses
   for its highlighted row). Applies to physical and stackable
   variants — the user always knows which slot the detail pane is
   showing.

2. Stackable backplane variant. New "stackable: true" flag on the
   module-slots section. When set:
     - Only populated slots are shown in the slot list.
     - A "+ Add module" button at the bottom appends a new slot
       populated with the first available module.
     - The "-- Empty --" option is dropped from the module picker.
     - A red "Remove module" button next to the picker opens a
       confirmation modal; on accept the slot is removed and all
       following slots shift up (slotsConfig keys and io-mapping
       slot numbers are renumbered too so aliases survive).
   Physical (default) behaviour is unchanged — vendors with a fixed
   chassis still use the existing fixed-slots flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The red trigger button stood out next to the rest of the Backplane
Configuration controls. Switched to the same neutral-outline style
the Clear All Slots button uses, so the row of controls reads as
a single visual group. The confirmation modal keeps its red accent
on the destructive action button, which is consistent with similar
confirmations elsewhere in the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clear All Slots wipes the whole backplane and releases every
allocated I/O address — destructive enough to warrant the same
confirmation pattern Remove module already uses. The screen JSON
action already had a `confirm` string declared but it was being
ignored; the local button now routes through a confirmation modal
when the action declares `confirm`, otherwise fires immediately
(unchanged for vendors that don't opt in).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each populated slot row now has a grab handle (DragHandleIcon, same
icon the ladder rungs use). Wrapping the slot list in a DndContext
+ SortableContext, the user can pick up a slot and drop it anywhere
in the list — the dragged module lands at the target index and
intermediate slots shift one position to make room.

Reorder mechanics:
  - arrayMove semantics for slots; slotsConfig keys re-mapped to
    follow each module's new position so per-slot module config
    (e.g. data_format selector) sticks with its module.
  - io-mapping entries' slot field is rewritten with the same
    re-map so user-typed aliases stay attached to their modules
    through the move. The io-mapping reallocator then re-assigns
    IEC addresses on the next render in the new slot order.
  - The selectedSlot follows the dragged module if it was the one
    being dragged, or shifts by ±1 if it was within the affected
    range, so the detail pane keeps showing the same module.

Empty slots are not draggable (no module to move); their drag-handle
column is reserved as inert whitespace to keep row layout aligned.
Applies to both physical and stackable variants.

Heads-up: IEC addresses (%IW, %ID, %IX, …) will renumber on reorder
because the auto-allocator iterates slots in order. Aliases survive
the renumber, so user-facing names are preserved, but the underlying
numeric addresses change. This matches the existing behaviour when a
slot is added/removed and is consistent with how the io-mapping
table is generated from scratch on every slots-array change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…target capabilities

Phase 0 of the alias source-of-truth work. Replaces every UI / feature
check that switched on `boardInfo.compiler === ...` with a capability
flag read from the board's TargetCapabilities block.

New shared module (backend/shared/utils/target-capabilities):
  - TargetCapabilities interface with 13 flags grouped into address
    producers (pinMapping, vppIo, modbusTcpRemote, ethercat), server
    protocols (modbusTcp/opcua/s7), debugger transports, and build /
    runtime behavior (pythonFunctionBlocks, arduinoApiCompletions,
    hasRuntimeStats, isInProcessSimulator, directUsbUpload).
  - Presets for Simulator, Runtime v3, Runtime v4, Arduino-CLI.
  - resolveTargetCapabilities(boardInfo) — single helper that all
    capability-relevant call sites use. Reads boardInfo.capabilities
    directly when present; falls back to the legacy `compiler` string
    only for hals.json files that haven't been migrated yet.
  - Pure and side-effect-free, lives in backend/shared so openplc-web
    inherits it byte-identically.

hals.json:
  - Explicit `capabilities` blocks on OpenPLC Simulator, Runtime v3,
    Runtime v4. Required because v3 and v4 both use the same
    `openplc-compiler` compiler value — the resolver's compiler-string
    fallback only knows v4 by default.
  - Simulator reports server / remote-IO support as true (no-ops at
    the bytecode level) so projects authored for Runtime v4 don't get
    nagged when the user switches to Simulator to test.

middleware/shared/ports/types.ts:
  - BoardInfo gains an optional `capabilities` partial block. Web's
    BoardInfo is structurally identical — same change ports verbatim.

Call-site migrations:
  - frontend/utils/device.ts: predicates reimplemented over the
    resolver. Public API unchanged; internals now compiler-string-free.
  - workspace-activity-bar/default.tsx: isSimulatorBoard now reads
    caps.isInProcessSimulator.
  - device/configuration/board.tsx: server / remote-device warning on
    target switch now driven by the new target's capabilities
    (modbusTcpServer/opcuaServer/s7Server and modbusTcpRemote/ethercat)
    instead of "is target v4 or simulator". Python-FB warning now reads
    caps.pythonFunctionBlocks.
  - device/orchestrators/orchestrators-list.tsx: simulator detection
    moved to caps.isInProcessSimulator. Kept here for openplc-web
    byte-parity.
  - editor/monaco/index.tsx: Arduino API completions now gated by
    caps.arduinoApiCompletions instead of the
    board-name.includes('OpenPLC Runtime') hack.

Build infrastructure (compiler-module, hardware-module, compiler-adapter)
left alone — it correctly switches on `compiler` to pick a toolchain;
those checks aren't capability decisions and aren't part of this scope.

14 unit tests for resolveTargetCapabilities covering preset shapes,
back-compat compiler-string fallback, explicit-capabilities override,
partial-block merge, and the web orchestrator-device path (capabilities
without a compiler field). Existing device.ts tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s pool

Phase 1 of the alias source-of-truth work. Replaces the two scattered
utilities (`collectUsedIecAddresses` + `generateIecAddress`) with a
single producer-only pool that target-scopes claims via the
capability matrix from Phase 0.

New module (`backend/shared/utils/iec-address`):
  - address-pool.ts: `buildAddressPool(inputs, caps, options?)` walks
    every producer that's active for the current target and emits
    a structure with `byAddress`, `byPrefix` (sorted), and a
    `conflicts` report when two producers claim the same address.
    Reservation pass for pin-mapping (Arduino-style fixed addresses)
    runs first so other allocators can't claim a pin-bound slot.
  - `nextFreeAddress(pool, prefix, isBit, startFrom?, alsoUsed?)`
    replaces `generateIecAddress`. The `alsoUsed` set lets bulk
    allocators track in-flight picks without rebuilding the pool
    between iterations.
  - `ignoreSource` (regenerate this producer) and
    `ignoreCapabilities` (the user is actively editing this producer
    so caps gating doesn't apply) are the two opt-ins.
  - 22 unit tests cover producer scoping, capability switching,
    conflict reporting, allocation, and prefix sorting.

Migrations (all consumers off the old utilities):
  - VPP io-table + module-slots layouts use the pool with
    `ignoreSource: 'vpp-io'` (they're regenerating VPP claims) plus
    `alsoUsed` for the in-flight reallocation batch.
  - EtherCAT device-editor and the bus editor build a pool and read
    its keys into a Set<string> (preserving the existing
    `externalAddresses` API). Same code path on both sites.
  - project/slice.ts `addIOGroup` rebuilds the pool with
    `ignoreCapabilities: true` (the user is actively editing the
    Modbus TCP producer, so target gating is intentionally bypassed).
  - `generateIOPoints` simplified: walks `nextFreeAddress` instead of
    the manual bit/word arithmetic; takes the pool + pending set.

Cleanups:
  - frontend/utils/iec-address.ts + its tests deleted (the old
    `generateIecAddress` is replaced by `nextFreeAddress`).
  - backend/shared/utils/iec-address/collect-used-iec-addresses.ts
    + its tests deleted.
  - `EtherCATChannelMapping.userEdited` field removed (dead since
    addresses are editor-allocated; both the TS interface and the
    Zod schema dropped, esi-parser stops setting it). Zod's default
    "strip unknown keys" behaviour keeps legacy projects loadable.

No call sites changed behaviorally — every existing flow allocates
the same addresses it did before, with target-scoping now correctly
releasing claims when capabilities change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the alias source-of-truth work. Adds the alias registry
(a derived view over the address pool) and brings pin-mapping in
line with the other producers by renaming `pin.name` -> `pin.alias`.
The pool already attached aliases to claims in Phase 1; this phase
gives the editor a way to look them up.

Alias registry (backend/shared/utils/iec-address/alias-registry.ts):
  - buildAliasRegistry(pool): pure function returning byAlias /
    byAddress indexes plus a duplicateAliases list (first-wins).
  - resolveAlias(reg, alias) — the address an alias currently
    points to. Returns undefined when orphaned.
  - aliasForAddress(reg, addr) — the alias attached to an address
    (the auto-adopt path for variables).
  - isAliasNameAvailable(reg, alias, ignoring?) — system-wide
    uniqueness check; an `ignoring` SourceRef lets a producer
    rename within itself without false positives.
  - 14 unit tests cover indexing, duplicate detection, target
    scoping, and the orphan path.

Pin.name -> Pin.alias rename (system-wide):
  - devicePinSchema (backend/shared/types/PLC/devices/pin.ts) renames
    the field and adds a Zod preprocess step that migrates legacy
    projects on load: `{ name: "x" }` -> `{ alias: "x" }`. Older
    project files self-upgrade with no user action.
  - DevicePin interface in middleware/shared/ports/types.ts updated
    in lockstep (byte-identical port shape for openplc-web).
  - PoolPinMappingInput.pin.name -> .alias; pin-mapping claims now
    feed the alias registry exactly like every other producer.
  - frontend/store/slices/device:
    * slice.ts new-pin defaults use `alias: ''` instead of
      `name: ''`; updatePin handles the renamed key.
    * validation/pins.ts: pinNameValidation -> pinAliasValidation,
      checkIfPinNameExists -> checkIfPinAliasExists,
      checkIfPinNameIsValid -> checkIfPinAliasIsValid (plus updated
      error message strings).
    * types.ts: PinUpdateResponse.data renamed to match.
  - UI:
    * pin-mapping-table.tsx column accessor + header label flipped
      from "Name" to "Alias".
    * variables-table + global-variables-table picker labels read
      pin.alias (cell label format unchanged).
  - Tests: 134 tests across device-pins-validation / device-slice /
    device-types / address-pool / alias-registry pass under the new
    names.

Full test suite: 3809 passing (the pre-existing use-runtime-polling
failure remains, unrelated to this work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 of the alias source-of-truth work. Variables become aware of
the alias registry: when bound to an alias they display the alias
name in the location cell, and any commit that lands on an
address-with-alias auto-adopts the alias.

Schema (backend/shared/types/PLC/open-plc.ts):
  - PLCVariableSchema gains optional `alias`. The field is purely
    a UI hint + Phase-4 sync anchor; the authoritative location for
    compile is still `location`. Schema bump is additive — older
    projects without the field load unchanged.

updateVariable (frontend/store/slices/project/slice.ts):
  - Whenever `updates.location` is supplied, the action rebuilds the
    alias registry from current store state and patches
    `updates.alias` to whatever the new address has bound (or
    undefined if nothing). This is the auto-adopt path. Re-committing
    the same address re-runs the lookup, which intentionally drops
    aliases that are now orphaned.
  - The pool is built without `ignoreCapabilities` here: alias
    resolution honours target scoping — switching to a target that
    deactivates a producer correctly orphans variables bound to its
    aliases.

Variable cells (variables-table + global-variables-table):
  - When not in edit mode, display the bound alias name in place of
    the raw IEC address; hover tooltip shows `alias -> address` so
    the underlying binding is one mouseover away.
  - When edit mode is open, the raw address is exposed so the user
    can rebind. The picker still commits an address; the
    auto-adopt in updateVariable patches the alias.
  - global-variables-table now reads row.original.alias (the editable
    cell already had row access; this just exposes it on the
    destructure).

Full test suite: 3809 passing (the pre-existing use-runtime-polling
failure remains, unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on site

Phase 4 of the alias source-of-truth work. Adds the pure sync
function and threads it through every site that mutates an alias-
producing source, the board switch, project load, and pre-compile.

Pure function (backend/shared/utils/iec-address/sync-variable-aliases.ts):
  - syncVariableAliases(variables, registry) walks an array of
    located variables and produces three outcomes:
      * adopted   — variable had no alias bound but its `location`
                    matches an aliased address. Self-upgrade path.
      * refreshed — variable has a known alias whose address has
                    moved; `location` follows the alias.
      * orphaned  — variable has an alias the registry no longer
                    knows about. Keeps `location` and `alias` so
                    the user can decide; reported for the UI.
  - Generic over any SyncableVariable shape so concrete subtypes
    (PLCVariable) thread through without losing fields.
  - 11 unit tests cover every outcome and the carry-through invariant.

Store action (projectActions.syncVariableAliases):
  - Builds pool + registry from live state under the active target's
    capabilities, then runs the pure function over every POU's
    interface variables and the global variables.
  - Returns a counts-only summary `{ adopted, refreshed, orphaned }`
    so callers can log one-liners.
  - Honors target scoping: switching to a target that disables a
    producer correctly orphans the variables that depended on it.

Wired triggers:
  - Project load (shared/slice.ts handleOpenProjectResponse): runs at
    the end of load so older projects self-upgrade on first open.
    Logs a single info-level summary when anything changed.
  - Pre-compile (workspace-activity-bar): the projectData handed to
    the compiler is now sync-fresh on both compile paths (regular
    build + MD5-mismatch confirm path).
  - VPP io-mapping reallocator effects in io-table-layout and
    module-slots-layout (every setVendorScreenData('io-mapping') the
    reallocator and the alias-edit handler emit).
  - VPP alias-edit handlers (handleAliasChange in both layouts).
  - Modbus TCP `updateIOPointAlias` action (project slice).
  - EtherCAT `syncDevicesToStore` (ethercat-device-editor) — covers
    channelMapping alias edits and bus re-scans.
  - Pin-mapping table: pin alias / address edits.
  - Board switch (board.tsx useEffect on deviceBoard) — covers every
    code path that ends up calling setDeviceBoard (regular pick,
    Python-warning confirm, V4-features-warning confirm).

The compile pipeline contract is unchanged — it still reads
`variable.location` verbatim. Sync just guarantees the location is
current before any read happens.

Full suite: 3820 passing (+12). Only pre-existing
use-runtime-polling failure remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 of the alias source-of-truth work. Surfaces orphans to the
user and documents the new system in CLAUDE.md.

New hook (frontend/hooks/use-alias-registry.ts):
  - useAliasRegistry() builds the registry from live store state
    via a memoized selector. Inputs subscribed individually so
    re-derivation only fires when one actually changes.

Variable cell orphan badge (variables-table + global-variables-table
editable-cell.tsx):
  - When a variable carries an alias the registry no longer knows
    about, the cell renders an amber warning glyph next to the
    label (the alias name itself stays — the user sees what the
    binding used to be, just flagged).
  - Hover tooltip explains the state: "Alias 'X' is no longer
    declared by any active source. Last known address: ...".
  - Text colour shifts to amber so the cell is obvious in a long
    variable table without dominating it.

CLAUDE.md:
  - New "IEC address allocation + alias registry" section under
    Important Patterns, summarising address-pool / alias-registry /
    sync-variable-aliases and the producer set. Points future
    contributors at backend/shared/utils/iec-address as the single
    source of truth.

Full suite: 3820 passing (the pre-existing use-runtime-polling
failure remains, unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…to-adopt

Two issues surfaced after the alias feature shipped:

1. Variable cell flipped between alias-name and IEC-address on row
   select/deselect — confusing because the same cell rendered
   differently depending on focus state. Now consistent:

   - GenericComboboxCell gains an optional `displayLabel` prop. When
     supplied, it overrides the trigger's visible text without
     changing the underlying value the combobox edits.
   - Both variables-table and global-variables-table compute
     `combinedLabel = "{alias} ({address})"` (or just the address
     when no alias is bound) and render it in BOTH the selected
     (combobox closed-state) and unselected (HighlightedText) paths.
   - Tooltip simplified: orphan state still shows its full message,
     non-orphan state shows `alias -> address`.
   - Editing (combobox open) still operates on the raw address — the
     user types/picks an address, the alias is patched in by the
     auto-adopt path.

2. Auto-adopt on project load didn't actually fire — the user had to
   visit the VPP screen and edit an alias to see variables pick up
   their bound names. Root cause: `availableBoards` is loaded
   asynchronously by workspace-screen, but the project-load handler
   runs the sync before that completes. The cap-resolver returns an
   empty capability block when the board info isn't available yet,
   so the address pool excludes every producer and the alias registry
   ends up empty — adoption walks zero entries.

   Two-pronged fix:

   - `syncVariableAliases({ ignoreCapabilities })` now accepts an
     opt-in to skip cap gating. The project-load handler in
     shared/slice.ts passes `ignoreCapabilities: true` so every alias
     declared anywhere in the project data is registered, regardless
     of which board the user picked.
   - workspace-screen.tsx's board-loading effect re-runs the sync
     (cap-gated this time) once availableBoards resolves, so orphan
     detection surfaces correctly for the active target without
     waiting for the user to navigate to device-config.

   Existing producer-mutation triggers and the board-switch effect
   continue running cap-gated by default — the bypass only applies
   to the initial load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…slice access, allocator + memo improvements

Addresses the review feedback on #762.

Architecture (the must-fix):
  - Moved `target-capabilities/` and `iec-address/` from
    `backend/shared/utils/` to `middleware/shared/utils/`. The arch
    validator now maps `middleware/shared/utils/` to the existing
    `utils` layer, so store / hooks / components can import them
    cleanly without violating store -> backend-shared rules.
  - validate:arch is green for this PR; only the two pre-existing
    violations in frontend/utils/{debug-tree-traversal,pou-helpers}
    remain.

Allocator (byPrefix utilisation):
  - Rewrote `nextFreeAddress` to merge the pool's sorted byPrefix
    list with the alsoUsed set and find the lowest unclaimed slot in
    one walk. Better address space utilisation for sparse claims
    (reclaims gaps the previous candidate-by-candidate scan would
    eventually find but only after walking past them).
  - Added a gap-fill test (sparse %IW claims), a non-bit %ID test
    fixture, and `nextFreeAddress(pool, '%ID', false, 3)` startFrom
    coverage.

Conflict surface:
  - syncVariableAliases now writes a `warning`-level entry to the
    console slice when `pool.conflicts` is non-empty. Includes
    the first 5 conflicting addresses with their source kinds.

Cross-slice typing (no more casts):
  - New `ProjectSliceRoot = ProjectSlice & DeviceSlice & ConsoleSlice`
    in project/types.ts.
  - createProjectSlice typed as `StateCreator<ProjectSliceRoot, ...>`
    so getState() returns the cross-slice union directly. The three
    `getState() as unknown as { ... }` casts (updateVariable, syncVariableAliases,
    addIOGroup) and the stale `pins as Array<{ name?: ... }>` cast
    are all gone.
  - Project-slice tests now compose project + device + console slices
    in their makeStore() helper.

Store-level alias registry cache:
  - use-alias-registry.ts swaps per-cell useMemo for a module-level
    single-entry cache keyed on input identity. N variable cells in
    the same render now share one pool build.

console.info → console slice:
  - Project-load sync summary routes through consoleActions.addLog
    so it shows up in the in-app console alongside other renderer logs.

DebuggerTransport drift:
  - middleware/shared/ports/types.ts now re-exports
    DebuggerTransport / TargetCapabilities from the utils module
    instead of redeclaring the union inline. Single source of truth.

PLCVariable.alias in middleware:
  - The interface in middleware/shared/ports/types.ts was missing the
    alias field added to the Zod schema in Phase 3. Caught when the
    new integration tests asserted `vars[0].alias`. Added it with the
    same docstring as the Zod schema.

New integration tests (sync-variable-aliases-action.test.ts):
  - 7 scenarios covering adopt, refresh, orphan, target switch,
    ignoreCapabilities bypass, POU + global atomic sync, and conflict
    routing to the console slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s the project-load sync

The ignoreCapabilities flag on buildAddressPool / syncVariableAliases
was a timing workaround that contradicted the whole capabilities
model: bypass mode let VPP / pin-mapping / EtherCAT claims leak into
the pool on targets whose caps didn't include them, breaking
allocation on target switch (e.g. VPP %IW0-%IW7 from a previous
SLM-RP4 session biasing a plain Runtime v4 Modbus allocation).

The real bug was call ordering at project load: handleOpenProjectResponse
ran sync before availableBoards landed, so caps resolved empty. Move
the sync into deviceActions.setAvailableOptions — it's the natural
project-load sync point (capabilities are accurate by then) and an
existing sync site for board-refresh from VPP package installs.

addIOGroup's defensive bypass goes away too: in real flows the
workspace screen always seeds boards before the user reaches the
remote-device editor. Tests that previously relied on the bypass
now seed a Runtime v4 board fixture.

Add a producer-agnostic test in the pure sync layer: build one
registry from VPP + Modbus + EtherCAT claims with each alias
relocated to a new IEC address, and assert every bound variable
follows its alias. The refresh path doesn't distinguish source kind,
so this single test covers the invariant for every producer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ruth

feat(alias): variables bind to producer-declared aliases with target-scoped allocation
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
CLAUDE.md (1)

293-294: 💤 Low value

Consider clarifying "byte-identical on openplc-web".

The phrase "byte-identical on openplc-web" may benefit from a brief clarification for readers unfamiliar with the relationship between the editor and web codebases. Does this mean the implementation is shared/copied, or that the behavior is equivalent?

Example clarification
-Located in `src/backend/shared/utils/iec-address/` (byte-identical on
-openplc-web). Pure functions, no IPC, no electron coupling.
+Located in `src/backend/shared/utils/iec-address/` (shared
+implementation with openplc-web). Pure functions, no IPC, no electron coupling.

or

-Located in `src/backend/shared/utils/iec-address/` (byte-identical on
-openplc-web). Pure functions, no IPC, no electron coupling.
+Located in `src/backend/shared/utils/iec-address/` (duplicated to
+openplc-web for consistency). Pure functions, no IPC, no electron coupling.

Note: The static analysis tool flagged "openplc-web" as a potential spelling error, but this is clearly a project name and can be safely ignored.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 293 - 294, Revise the sentence mentioning
"byte-identical on openplc-web" to explicitly state whether the implementation
files are copied/shared or merely behavior-equivalent; update the text near
src/backend/shared/utils/iec-address/ to say e.g. "the implementation is copied
from openplc-web (byte-identical)" or "the implementation matches openplc-web
behavior (not copied)" and add a short parenthetical noting that "openplc-web"
is a project name to avoid static-analysis spelling flags.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@CLAUDE.md`:
- Around line 293-294: Revise the sentence mentioning "byte-identical on
openplc-web" to explicitly state whether the implementation files are
copied/shared or merely behavior-equivalent; update the text near
src/backend/shared/utils/iec-address/ to say e.g. "the implementation is copied
from openplc-web (byte-identical)" or "the implementation matches openplc-web
behavior (not copied)" and add a short parenthetical noting that "openplc-web"
is a project name to avoid static-analysis spelling flags.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8789590c-2ddd-407e-867a-92149311d265

📥 Commits

Reviewing files that changed from the base of the PR and between 4c08d60 and 2e05059.

⛔ Files ignored due to path filters (30)
  • resources/sources/boards/hals.json is excluded by !resources/**
  • resources/sources/hal/arduino_opta.cpp is excluded by !resources/**
  • resources/sources/hal/controllino_maxi.cpp is excluded by !resources/**
  • resources/sources/hal/controllino_maxi_automation.cpp is excluded by !resources/**
  • resources/sources/hal/controllino_mega.cpp is excluded by !resources/**
  • resources/sources/hal/controllino_micro.cpp is excluded by !resources/**
  • resources/sources/hal/controllino_mini.cpp is excluded by !resources/**
  • resources/sources/hal/edge_control.cpp is excluded by !resources/**
  • resources/sources/hal/esp32.cpp is excluded by !resources/**
  • resources/sources/hal/esp8266.cpp is excluded by !resources/**
  • resources/sources/hal/fx3u-14-WS3U.cpp is excluded by !resources/**
  • resources/sources/hal/fx3u-14.cpp is excluded by !resources/**
  • resources/sources/hal/fx3u-24MR-DT.cpp is excluded by !resources/**
  • resources/sources/hal/giga.cpp is excluded by !resources/**
  • resources/sources/hal/iruinoVEA.cpp is excluded by !resources/**
  • resources/sources/hal/jaguar.cpp is excluded by !resources/**
  • resources/sources/hal/machine_control.cpp is excluded by !resources/**
  • resources/sources/hal/mega_due.cpp is excluded by !resources/**
  • resources/sources/hal/mkr.cpp is excluded by !resources/**
  • resources/sources/hal/nano_every.cpp is excluded by !resources/**
  • resources/sources/hal/p1am.cpp is excluded by !resources/**
  • resources/sources/hal/rp2040.cpp is excluded by !resources/**
  • resources/sources/hal/rp2040pico.cpp is excluded by !resources/**
  • resources/sources/hal/sequent_esp32.cpp is excluded by !resources/**
  • resources/sources/hal/stm32_f103cb.cpp is excluded by !resources/**
  • resources/sources/hal/stm32_f411ce.cpp is excluded by !resources/**
  • resources/sources/hal/stm32_f446zet_nucleo.cpp is excluded by !resources/**
  • resources/sources/hal/uno_leonardo_nano_micro_zero.cpp is excluded by !resources/**
  • resources/sources/hal/uno_q.cpp is excluded by !resources/**
  • resources/sources/hal/uno_r4.cpp is excluded by !resources/**
📒 Files selected for processing (6)
  • CLAUDE.md
  • src/__architecture__/validate.ts
  • src/backend/editor/compiler/compiler-module.ts
  • src/backend/shared/ethercat/esi-parser.ts
  • src/backend/shared/ethercat/generate-ethercat-config.ts
  • src/backend/shared/ethercat/index.ts

thiagoralves and others added 3 commits May 20, 2026 17:33
…, and text-mode variable commits

Drop the "Project opened!", "Changes saved!", "File saved", "Variables
updated" and "Global Variables updated" success notifications. They
fire on every routine save / load / text-mode flip, adding noise
without information the user can't already see from the editor state.
Error and warn toasts (parse failure, save failure, file-not-found,
no-file-open) are kept so the user is still told when something needs
attention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mposite generics

The ladder editor's local validateVariableType iterated genericTypeSchema.shape[X].options assuming every entry was either a string or a ZodLiteral that resolved to a flat list of base-type strings. Composite generics (ANY_NUM, ANY_INTEGRAL, ANY_MAGNITUDE, ANY_CHARS, ANY_ELEMENTARY) violate that assumption — their unions are made of ZodLiteral instances pointing back into the schema, so resolving them requires walking the graph. Adding an EQ block (input type ANY_ELEMENTARY) hit a ZodLiteral inside the recursion and threw "subValue.toLowerCase is not a function".

The FBD utils wrapper already delegated to frontend/utils/PLC/validate-variable-type which carries a correct recursive flattener. Replace the ladder duplicate with calls into the same shared helper, eliminating the duplicate code path entirely.

Regression covered by validate-variable-type.test.ts (ANY_NUM, ANY_INTEGRAL, ANY_MAGNITUDE, ANY_ELEMENTARY).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
applyDynamicBlockHandleOffsets has two branches: an expansion path (block grew because of branch overlaps) and a reset path (no expansion needed — restore default). The reset path was hardcoded to clobber every block's height with blockStyle.height (= DEFAULT_BLOCK_HEIGHT = 128), regardless of how many input/output handles the block has.

That works for typical 3-handle blocks but clips large function blocks: OSCAT SEQUENCE_4 declares 15 inputs and 8 outputs, so its natural frame is 620px tall. The reset pass overwrote it to 128px on every layout pass, leaving the bounding box sized for 3 connectors while IN3, START, RST, WAIT0..3, DELAY0..3, STOP_ON_ERROR, Q3, QX, RUN, STEP, STATUS rendered floating below the box.

Compute naturalHeight from maxHandles using the same formula getBlockSize uses (FIRST_HANDLE_Y + (maxHandles - 1) * DEFAULT_OFFSET + 24) and apply it in the reset branch instead of the constant. Small blocks still get their default-equivalent height; large blocks finally fit their handles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant