Skip to content

fix(iec-address): close the paths where address recompaction silently no-ops (DOPE-440) - #1005

Closed
JulioSergioFS wants to merge 5 commits into
developmentfrom
bugfix/DOPE-440-iec-address-recompaction-gaps
Closed

JulioSergioFS wants to merge 5 commits into
developmentfrom
bugfix/DOPE-440-iec-address-recompaction-gaps

Conversation

@JulioSergioFS

@JulioSergioFS JulioSergioFS commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Pull request info

Description of the changes proposed

The scenario named in the ticket was already fixed. The central registry work
(Phases 3–6, July) made deleteIOGroup call recalculateIecAddresses(), and the test
recompacts the surviving groups into the freed slots (bug #4) has passed ever since.
The ticket predates that work and was never re-verified. What remained are the paths
where the recompaction silently no-ops, plus the ticket's own "May also affect other
plugins" clause — which turns out to be literally true for pin mapping.

  • An unresolved target is not a target without capabilities. resolveTargetCapabilities(undefined)
    answers EMPTY_CAPABILITIES; feeding that to the allocator filters out every consumer,
    so assignments comes back empty and the write-back leaves the stale addresses in place
    while reporting success. A board id fails to resolve whenever the VPP package isn't
    installed, the project was authored elsewhere, or the catalogue hasn't loaded yet. A board
    that answered is now honoured exactly as declared (the deliberate kind-deactivation on
    target switch is untouched); a board that did not resolve allocates with every producer
    active.
  • Duplicate addresses were being persisted. The provisional pool had the same conflation,
    so on a Runtime v3 or unresolved target every new group restarted at %IW0.
    modbus-tcp-remote is now always counted in its own pool. This also collapses the three
    duplicated ~25-line pool preambles into one helper.
  • Pin mapping never recompacted. removePin slides the freed slot to the end of the pin
    block; createNewPin can mint an address already held by a VPP or Modbus channel, with no
    conflict report. All three pin actions now compare the pin address signature and recalculate
    only when it moves, so alias-only and pin-number-only edits stay free.
  • Docs: new §5.1 in docs/iec-address-registry.md capturing the invariant
    "unknown target ⇒ permissive for allocation, empty for feature gating".

Summary by CodeRabbit

  • New Features

    • Remote-device creation and navigation are now shown only when supported by the selected target or when configured devices already exist.
    • Unresolved targets continue allocating addresses instead of leaving assignments empty.
  • Bug Fixes

    • IEC addresses now recalculate after pin additions, removals, or type changes, preventing stale addresses and conflicts.
    • Alias-only and pin-number-only edits no longer trigger unnecessary recalculation.
    • Modbus address pools consistently include remote consumers.
  • Documentation

    • Added guidance on capability-scoped allocation and unresolved-target handling.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba222575-c239-416d-aed6-053dacb21e1f

📥 Commits

Reviewing files that changed from the base of the PR and between 709e6a9 and 1abc55b.

📒 Files selected for processing (2)
  • src/frontend/store/__tests__/device-slice.test.ts
  • src/frontend/store/slices/device/slice.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/frontend/store/tests/device-slice.test.ts
  • src/frontend/store/slices/device/slice.ts

Walkthrough

The change adds capability-scoped IEC allocation, permissive allocation for unresolved targets, centralized Modbus pools, target-aware remote-device handling, and conditional recalculation after pin layout changes. Tests cover resolved, unresolved, incapable, and pin-edit scenarios.

Changes

IEC address allocation and recalculation

Layer / File(s) Summary
Address producer capability contracts
src/middleware/shared/utils/target-capabilities/*, src/middleware/shared/utils/iec-address/address-pool.ts
AddressProducerCapabilities narrows allocation inputs to address-producing capabilities. ALL_ADDRESS_PRODUCERS_ACTIVE supports unresolved-target allocation.
Capability-aware project allocation
src/frontend/store/slices/project/slice.ts, src/frontend/store/__tests__/project-slice.test.ts
Resolved targets use supported producer kinds. Unresolved targets use all producers. Modbus pools support edited-group clearing. Tests cover recompaction, duplicate prevention, warnings, and alias validation.
Pin edit recalculation
src/frontend/store/slices/device/slice.ts, src/frontend/store/__tests__/device-slice.test.ts
Pin creation, removal, and address-changing updates trigger IEC recalculation. Alias-only and pin-number-only edits do not.
Target-aware remote-device handling
src/frontend/components/_features/[workspace]/create-element/index.tsx, src/frontend/components/_organisms/explorer/project.tsx, docs/iec-address-registry.md
Remote-device creation and visibility use Modbus TCP remote I/O or EtherCAT target capabilities. Existing remote devices remain visible on unsupported targets. Documentation describes the allocation rules and recalculation behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: ⚪ Minimal · up to 1abc5

The PR is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant DeviceSlice
  participant ProjectSlice
  participant IECAddressRegistry
  participant AddressPool
  DeviceSlice->>DeviceSlice: compare ordered pin-address layout
  DeviceSlice->>ProjectSlice: request recalculation when addresses change
  ProjectSlice->>IECAddressRegistry: rebuild project-wide registry
  IECAddressRegistry->>AddressPool: allocate with resolved or permissive capabilities
  AddressPool-->>IECAddressRegistry: return IEC address assignments
  IECAddressRegistry-->>ProjectSlice: update registry and allocations
Loading

Possibly related PRs

Suggested labels: enhancement

Poem

A rabbit maps each pin in line,
Modbus slots regroup and shine.
Unknown boards keep producers near,
Changed addresses trigger cheer.
Alias edits leave maps clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes clearly but omits the required References, Jira task, and DOD checklist sections. Add the missing References, Jira task, and DOD checklist sections, and complete each applicable checklist item.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the IEC address recompaction fix and its primary behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/DOPE-440-iec-address-recompaction-gaps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/store/slices/project/slice.ts (1)

265-272: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate vendorScreenData['io-mapping'] before address allocation.

deviceConfigurationSchema validates vendorScreenData only as Record<string, unknown>. Invalid entries values can make recalculateIecAddresses() throw or create malformed registry keys. Validate the nested shape with Zod or a type guard, and remove the type assertion at lines 265-272.

🤖 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 `@src/frontend/store/slices/project/slice.ts` around lines 265 - 272, Update
readVppEntries to validate vendorScreenData['io-mapping'] before returning
entries, using the existing Zod validation approach or a type guard to accept
only an object with a VppMappingEntry[] entries property; return an empty array
for invalid values. Remove the current type assertion and ensure
recalculateIecAddresses receives only validated entries.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@src/frontend/store/__tests__/device-slice.test.ts`:
- Around line 1792-1793: Update modbusAddresses to remove all non-null
assertions and explicitly validate each intermediate fixture value: the first
remote device, its modbusTcpConfig, ioGroups entry, and ioPoints. Throw a
diagnostic error identifying the missing fixture data when any value is absent,
then map the validated IO points to iecLocation.

---

Outside diff comments:
In `@src/frontend/store/slices/project/slice.ts`:
- Around line 265-272: Update readVppEntries to validate
vendorScreenData['io-mapping'] before returning entries, using the existing Zod
validation approach or a type guard to accept only an object with a
VppMappingEntry[] entries property; return an empty array for invalid values.
Remove the current type assertion and ensure recalculateIecAddresses receives
only validated entries.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8378ad7b-5e15-41ea-977b-4f0611120f22

📥 Commits

Reviewing files that changed from the base of the PR and between 6e7584c and 8ef397e.

📒 Files selected for processing (9)
  • docs/iec-address-registry.md
  • src/frontend/store/__tests__/device-slice.test.ts
  • src/frontend/store/__tests__/project-slice.test.ts
  • src/frontend/store/slices/device/slice.ts
  • src/frontend/store/slices/project/slice.ts
  • src/middleware/shared/utils/iec-address/address-pool.ts
  • src/middleware/shared/utils/target-capabilities/index.ts
  • src/middleware/shared/utils/target-capabilities/presets.ts
  • src/middleware/shared/utils/target-capabilities/types.ts

Comment thread src/frontend/store/__tests__/device-slice.test.ts Outdated
@Gustavohsdp

Copy link
Copy Markdown
Contributor

Review — good to go after one small test cleanup

Reviewed together with the web sync PR Autonomy-Logic/openplc-web#664 — the 8 shared files are byte-identical; docs/iec-address-registry.md exists only here.

Read against the ticket (DOPE-440: "IEC addresses not recalculated after a gap … May also affect other plugins", High) rather than just the diff.

Verified: 8/8 shared files byte-identical (git hash-object); 440 tests green (device-slice, project-slice); CI 12/12 editor, 9/9 web.

The shape of this PR is right

"The scenario named in the ticket was already fixed" — and saying so, then going after the paths where the recompaction silently no-ops, is more useful than either re-fixing it or closing as cannot-reproduce. The core is one function and it reads clearly:

function allocationCapabilities(live: ProjectSliceRoot): AddressProducerCapabilities {
  const boardInfo = resolveBoardInfo(live)
  return boardInfo ? resolveTargetCapabilities(boardInfo) : ALL_ADDRESS_PRODUCERS_ACTIVE
}

The bug underneath it is worth restating because it's the kind that hides forever: resolveTargetCapabilities(undefined) answered EMPTY_CAPABILITIES, which filtered out every consumer, so assignments came back empty and the write-back left the stale addresses in place while reporting success. Silent success is the worst failure mode there is, and the reachability list (VPP not installed, project authored elsewhere, catalogue not loaded yet) is ordinary rather than exotic.

Separating "permissive for allocation" from "empty for feature gating" into two distinct presets, instead of overloading one, is what makes that fix safe to reason about.

The ticket's "may also affect other plugins" clause

That turned out literally true for pin mapping, and finding it required taking the clause seriously rather than treating it as boilerplate: removePin slid the freed slot to the end of the pin block instead of recompacting, and createNewPin could mint an address already held by a VPP or Modbus channel with no conflict report.

The guard on top of it is the detail I'd single out — all three pin actions compare the pin address signature and recalculate only when it moves, so alias-only and pin-number-only edits stay free. That's what keeps a correctness fix from turning into a performance regression on every keystroke.


🟡 1 — CodeRabbit's open finding: non-null assertions in the test helper

device-slice.test.ts:1793:

store.getState().project.data.remoteDevices![0].modbusTcpConfig!.ioGroups[0].ioPoints!.map((p) => p.iecLocation)

Why it's worth doing: CLAUDE.md is explicit ("No non-null assertions (!): handle the undefined case or narrow explicitly"), and it's the same class you fixed on request in #989 and #994 — the throwing-helper pattern from #989 (getColorsDataType()) drops straight in here. Three assertions in one line, one helper. The thread has no reply yet, so it reads as overlooked rather than declined.

Note this file is mirrored, so the fix has to land in both repos byte-identically.

🟡 2 — Document the consequence of "unknown ⇒ permissive", next to the invariant

§5.1 records "unknown target ⇒ permissive for allocation, empty for feature gating", which is the right invariant. What it doesn't record is what follows from it: a project authored while the target is unresolved allocates with every producer active, so when the VPP is later installed and the target resolves, capabilities shrink and the addresses recompact — the user sees addresses move on the first open after installing the package.

Why it's worth a sentence. For alias-bound variables that's transparent, which is what the registry exists for. For variables bound to a literal %addr the binding doesn't follow, and the amber orphan/collision glyph is what catches it. That's acceptable behaviour — and strictly better than the old symptom of stale addresses reported as success — but someone reading §5.1 to understand the model won't derive "resolving later can move addresses" from the invariant alone. One line beside it closes the loop.


Coordination

This pair and the DOPE-439 pair both touch src/frontend/store/slices/project/slice.ts in different regions (address allocation here, addIOGroup/updateIOGroup there), so expect a mechanical conflict for whichever merges second. Keep each pair's two repos merging in short sequence — the shared-surface gate only tolerates divergence while an open PR exists in the other repo.

Review assisted by Claude Code.

@thiagoralves

Copy link
Copy Markdown
Contributor

On the third bullet — forcing modbus-tcp-remote active in the provisional pool

I reviewed this PR for conceptual correctness rather than line-level style. Two of the three fixes hold up well, and I want to record that before the objection:

  • Pin edits driving the recalculation is right. I went in expecting this to be wrong, on the assumption that pin mapping is Arduino-only — it isn't any more. com.openplc.raspberry-pi's raspberry-pi device is target.type: runtime-v4 with capabilities: { pinMapping: true }, so after the merge in resolveTargetCapabilities it carries pinMapping and modbusTcpRemote and ethercat at once. Pins and Modbus really do share one address space on a shipping board, and since pin-mapping stays out of ALLOCATED_KINDS no pin address is ever reallocated — the recalculation only recompacts the producers around the moved constraints, exactly like setDeviceBoard already does. The createNewPin collision it closes is a genuine persisted duplicate.
  • The unresolved-target diagnosis is real. Empty activeKinds → allocate.ts:53 filters every consumer → assignments = {} → applyModbusAddresses only writes when info.address is truthy, so everything freezes and the action still returns ok(). Confirmed in code.

The one I'd like changed before merge is buildModbusProducerPool forcing modbusTcpRemote: true.

The bug is real, but the fix leaves two answers to one question

The failure mode is exactly as described: pending is created fresh per addIOGroup call and the pool drops sibling Modbus claims at address-pool.ts:227, so every group restarts at %IW0 and duplicates get persisted. No argument there.

What I don't want to ship is the resulting split: after this change the codebase holds two contradictory answers to "is modbus-tcp-remote an active producer?" — forced on in buildModbusProducerPool, still off in recalculateIecAddresses via activeKindsFromCapabilities. On a Runtime v3 target that means IO-group addresses are assigned once by the provisional pool and then never managed again: deleteIOGroup won't recompact them, gaps never close, and no conflict is reported. It heals only if the user later switches to a capable target. Of the three possible states (UI gated / allocator manages it / neither), that hybrid is the least consistent one, and the whole point of the central registry was to have exactly one authority on this.

The root cause is a UI gate, not an allocator rule

The PR names the real cause in its own comment — "the Remote Devices branch is gated on the project TYPE instead" — and then works around it. That gate is projectCaps.hasRemoteDevices (ports/types.ts:514; false for libraries, true for PLC projects) at explorer/project.tsx:455 and create-element/index.tsx:37. Nothing there consults the target.

The rest of the codebase already behaves as though that combination is unsupported:

  • board.tsx:346 computes targetCantHostRemoteIo = !targetCaps.modbusTcpRemote && !targetCaps.ethercat and warns the user they're "losing remote I/O" on switch — then never enforces it afterwards.
  • location-dropdown-options.ts:105 hides remote-device option groups from the variable location dropdown when both flags are off. So on Runtime v3 today you can create IO points that no variable can reference — which to me is the clearest signal that the missing piece is the UI gate, not the pool.

And modbusTcpRemote: false for v3 looks correct on the merits: the v3 branch of the build (compile/pipeline.ts:671+) uploads a single program.st and delivers no remote-I/O configuration at all. v3 manages its slave devices in its own web UI. So the editor genuinely cannot honour that data on v3, and flipping RUNTIME_V3_CAPABILITIES.modbusTcpRemote to true would be the wrong way out.

What I'd like instead

Gate remote-device creation on the capability — modbusTcpRemote || ethercat, the same predicate board.tsx:346 and location-dropdown-options.ts:105 already use — at the explorer branch and the create-element path. With that in place "capability off" and "screen unreachable" coincide for remote devices exactly as they already do for pin mapping, VPP and EtherCAT, the special case in buildModbusProducerPool becomes unnecessary, and capability scoping stays the single source of truth.

Note that the duplicate-address scenario the override fixes then only remains reachable on an unresolved target, where ALL_ADDRESS_PRODUCERS_ACTIVE already turns the kind on — so the override has no remaining job. The helper itself is a good consolidation and worth keeping; it's only the { ...allocationCapabilities(live), modbusTcpRemote: true } spread that should go.

Happy for the UI gate to land as a separate ticket if you'd rather keep this PR tight — in that case please drop the override here and let the v3 duplicate stand until the gate lands, rather than merging a workaround that becomes dead code a week later.

Smaller things, non-blocking

  • Narrowing buildAddressPool to AddressProducerCapabilities is a good change — it makes "not a target" representable in the type instead of by convention.
  • Please add a log line when allocation runs against an unresolved target. Permissive is the right default, but this class of bug survived months precisely because it was silent, and ALL_ADDRESS_PRODUCERS_ACTIVE is invisible from the outside too.
  • The new ?. chains in readVppEntries / resolveBoardInfo read as noise next to buildIecRegistry, which reads the same slices non-optionally a few lines away. One style or the other.
  • expect(writes()).toBe(2) as a proxy for "did it recalculate" will break on any unrelated extra setState. The address assertions in those same tests already prove the behaviour.
  • Worth deciding what happens to DOPE-440 itself: per the description its own scenario has been passing since July, so this PR is really "adjacent gaps found while verifying". Probably close the ticket as already-fixed and track these under their own key.

Longer term, the fact that a project stores only a board name is what makes the unresolved case possible at all. Persisting the four producer flags into the project file would mean an uninstalled package never loses target knowledge, and ALL_ADDRESS_PRODUCERS_ACTIVE would become a true last resort. Follow-up, not this PR.

@JulioSergioFS
JulioSergioFS deleted the bugfix/DOPE-440-iec-address-recompaction-gaps branch August 13, 2026 21:48
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.

3 participants