feat(gem): multi-observer state-change handlers via HandlerSlot

The single-slot set_*_handler pattern was a structural blocker, hit twice:
the daemon could not observe control-state changes because
register_default_handlers owns the slot, forcing GetControlState to read the
FSM cross-thread (a data race), and blocking WatchHealth and the Subscribe
stream's ControlStateChange variant.

HandlerSlot<Args...> keeps a primary slot with exact legacy semantics
(set_ replaces — one existing test depends on replacement) plus an
append-only observer list (add_) that survives set_ calls. Fire sites are
textually unchanged (operator bool / operator() / assign-from-function).

Applied to ControlStateMachine + ProcessJobStore + ControlJobStore (the
roadmap-critical three; the remaining single-slot classes follow the same
3-line pattern as needed). EquipmentRuntime gains an atomic control-state
mirror registered as an observer — control_state() is now safe from any
thread, retiring the GetControlState race — plus add_control_state_observer
and add_link_observer (selected/closed fan-out), the hooks WatchHealth and
Subscribe need.

Tests: observer ordering, set-replaces-primary-but-observers-survive,
observers-without-primary, PJ-store coexistence, and the runtime scenario
that was previously impossible (mirror + observer + default-handlers set_).
Core 464/464 (2816 assertions), daemon 16/16, live GEM300 demo passes with
single-fire control-state transitions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 18:57:53 +02:00
parent b067a76b80
commit 8a48ffeed4
10 changed files with 192 additions and 7 deletions
+6 -1
View File
@@ -6,6 +6,8 @@
#include <string>
#include <vector>
#include "secsgem/gem/handler_slot.hpp"
namespace secsgem::gem {
// E30 §6.2 Control State Model.
@@ -94,7 +96,10 @@ class ControlStateMachine {
ControlState state() const { return state_; }
bool online() const { return is_online(state_); }
// Replaces the primary handler (legacy single-slot semantics).
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
// Appends an observer that survives set_state_change_handler calls.
void add_state_change_handler(StateChangeHandler h) { on_change_.add(std::move(h)); }
// Operator actions. Return true if a transition (or self-ack) was found.
bool operator_online();
@@ -113,7 +118,7 @@ class ControlStateMachine {
ControlTransitionTable table_;
ControlState state_;
StateChangeHandler on_change_;
HandlerSlot<ControlState, ControlState, ControlEvent> on_change_;
};
} // namespace secsgem::gem