Documentation
¶
Overview ¶
Package conditions implements the condition "timer"/"threshold"/"counter" block family.
Phase 1 provides the shared infrastructure only: the behavior attribute bundle common to every subtype, and the state machine that drives activate_after / deactivate_after / timeout / cooldown / latch / invert semantics. Subtype registrations (timer, threshold, counter) are added in later phases.
Index ¶
- Variables
- type Behavior
- type Clock
- type ClockTimer
- type CounterCondition
- func (c *CounterCondition) Clear(ctx context.Context) error
- func (c *CounterCondition) Count(_ context.Context) (int64, error)
- func (c *CounterCondition) Get(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (c *CounterCondition) Increment(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (c *CounterCondition) PostStart() error
- func (c *CounterCondition) Reset(ctx context.Context) error
- func (c *CounterCondition) Start() error
- func (c *CounterCondition) State(ctx context.Context) (string, error)
- func (c *CounterCondition) Stop() error
- func (c *CounterCondition) Unwatch(w richcty.Watcher)
- func (c *CounterCondition) Watch(w richcty.Watcher)
- type FlipflopCondition
- func (f *FlipflopCondition) Clear(ctx context.Context) error
- func (f *FlipflopCondition) Get(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (f *FlipflopCondition) PostStart() error
- func (f *FlipflopCondition) Set(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (f *FlipflopCondition) Start() error
- func (f *FlipflopCondition) State(ctx context.Context) (string, error)
- func (f *FlipflopCondition) Stop() error
- func (f *FlipflopCondition) Toggle(ctx context.Context, _ []cty.Value) (cty.Value, error)
- func (f *FlipflopCondition) Unwatch(w richcty.Watcher)
- func (f *FlipflopCondition) Watch(w richcty.Watcher)
- type HookDispatcher
- type Hooks
- type RealClock
- type State
- type StateMachine
- func (sm *StateMachine) Bootstrap()
- func (sm *StateMachine) Clear(ctx context.Context) error
- func (sm *StateMachine) Get(_ context.Context, _ []cty.Value) (cty.Value, error)
- func (sm *StateMachine) Output() bool
- func (sm *StateMachine) RawInput() bool
- func (sm *StateMachine) SetInhibited(ctx context.Context, inhibited bool)
- func (sm *StateMachine) SetRawInput(ctx context.Context, value bool)
- func (sm *StateMachine) State(_ context.Context) (string, error)
- type ThresholdCondition
- func (c *ThresholdCondition) Clear(ctx context.Context) error
- func (c *ThresholdCondition) Get(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (c *ThresholdCondition) PostStart() error
- func (c *ThresholdCondition) Start() error
- func (c *ThresholdCondition) State(ctx context.Context) (string, error)
- func (c *ThresholdCondition) Stop() error
- func (c *ThresholdCondition) Unwatch(w richcty.Watcher)
- func (c *ThresholdCondition) Watch(w richcty.Watcher)
- type TimerCondition
- func (t *TimerCondition) Clear(ctx context.Context) error
- func (t *TimerCondition) Get(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (t *TimerCondition) PostStart() error
- func (t *TimerCondition) Set(ctx context.Context, args []cty.Value) (cty.Value, error)
- func (t *TimerCondition) Start() error
- func (t *TimerCondition) State(ctx context.Context) (string, error)
- func (t *TimerCondition) Stop() error
- func (t *TimerCondition) Toggle(ctx context.Context, _ []cty.Value) (cty.Value, error)
- func (t *TimerCondition) Unwatch(w richcty.Watcher)
- func (t *TimerCondition) Watch(w richcty.Watcher)
Constants ¶
This section is empty.
Variables ¶
var CounterConditionCapsuleType = cty.CapsuleWithOps("counter_condition", reflect.TypeOf((*CounterCondition)(nil)).Elem(), &cty.CapsuleOps{ GoString: func(v interface{}) string { return fmt.Sprintf("counter_condition(%p)", v) }, TypeGoString: func(_ reflect.Type) string { return "CounterCondition" }, })
var FlipflopConditionCapsuleType = cty.CapsuleWithOps("flipflop_condition", reflect.TypeOf((*FlipflopCondition)(nil)).Elem(), &cty.CapsuleOps{ GoString: func(v interface{}) string { return fmt.Sprintf("flipflop_condition(%p)", v) }, TypeGoString: func(_ reflect.Type) string { return "FlipflopCondition" }, })
var ThresholdConditionCapsuleType = cty.CapsuleWithOps("threshold_condition", reflect.TypeOf((*ThresholdCondition)(nil)).Elem(), &cty.CapsuleOps{ GoString: func(v interface{}) string { return fmt.Sprintf("threshold_condition(%p)", v) }, TypeGoString: func(_ reflect.Type) string { return "ThresholdCondition" }, })
var TimerConditionCapsuleType = cty.CapsuleWithOps("timer_condition", reflect.TypeOf((*TimerCondition)(nil)).Elem(), &cty.CapsuleOps{ GoString: func(v interface{}) string { return fmt.Sprintf("timer_condition(%p)", v) }, TypeGoString: func(_ reflect.Type) string { return "TimerCondition" }, })
Functions ¶
This section is empty.
Types ¶
type Behavior ¶
type Behavior struct {
ActivateAfter time.Duration
DeactivateAfter time.Duration
Timeout time.Duration
Cooldown time.Duration
Latch bool
Invert bool
Retentive bool
StartActive bool
Inhibit hcl.Expression
}
Behavior holds the attributes common to every condition subtype. Zero-valued durations disable their respective delays. Inhibit is captured as the raw HCL expression; Phase 2 (reactive expression infrastructure) wires it into the state machine.
type Clock ¶
type Clock interface {
Now() time.Time
AfterFunc(d time.Duration, f func()) ClockTimer
}
Clock abstracts time so the state machine is testable. Production code uses RealClock; tests use a fake implementation.
type ClockTimer ¶
type ClockTimer interface {
Stop() bool
}
ClockTimer is the subset of time.Timer the state machine uses.
type CounterCondition ¶
type CounterCondition struct {
// contains filtered or unexported fields
}
CounterCondition is the `condition "counter"` subtype. It tracks a running integer count via increment()/decrement() and produces a boolean output via the shared StateMachine when the count reaches a configured preset.
Counter does not support input=, debounce, or retentive (per spec §Attribute Applicability) — those are rejected at parse time. The shared behavioral attributes (activate_after / deactivate_after / timeout / latch / invert / cooldown / inhibit) are honored.
func (*CounterCondition) Clear ¶ added in v0.35.0
func (c *CounterCondition) Clear(ctx context.Context) error
Clear implements richcty.Clearable. For counters there is no `input =` to re-sample (counters reject input=), so clear() is equivalent to reset(): it empties the count (and the windowed FIFO), releases any latch, and returns the output to inactive.
func (*CounterCondition) Count ¶
func (c *CounterCondition) Count(_ context.Context) (int64, error)
Count implements richcty.Countable: returns the current numeric count value. Distinct from Get(), which returns the boolean preset-reached output.
In windowed mode, expired events are pruned opportunistically before the count is read so that a quiescent counter still reports an accurate value between expiry-timer firings.
func (*CounterCondition) Increment ¶
Increment implements richcty.Incrementable. Adds args[0] (default 1) to the count, applies low-side clamp at 0 (per spec — applies to decrement under rollover=false; we apply uniformly since a bare increment with negative delta is decrement), then re-evaluates the preset comparison and drives the state machine. With rollover=true, reaching the preset auto-resets the count to initial; under latch=true the latched output rides through the auto-reset so no spurious deactivate edge fires.
func (*CounterCondition) PostStart ¶ added in v0.35.0
func (c *CounterCondition) PostStart() error
PostStart fires the on_init hook after all Startables have bootstrapped. Implements config.PostStartable.
func (*CounterCondition) Reset ¶
func (c *CounterCondition) Reset(ctx context.Context) error
Reset implements richcty.Resettable: count → initial, latch released, pending state cancelled, output → inactive. Spec §Functions. In windowed mode, the FIFO of in-window events is also discarded and the expiry timer cancelled.
func (*CounterCondition) Start ¶
func (c *CounterCondition) Start() error
func (*CounterCondition) State ¶
func (c *CounterCondition) State(ctx context.Context) (string, error)
func (*CounterCondition) Stop ¶
func (c *CounterCondition) Stop() error
func (*CounterCondition) Unwatch ¶
func (c *CounterCondition) Unwatch(w richcty.Watcher)
func (*CounterCondition) Watch ¶
func (c *CounterCondition) Watch(w richcty.Watcher)
type FlipflopCondition ¶ added in v0.41.0
type FlipflopCondition struct {
// contains filtered or unexported fields
}
FlipflopCondition is the `condition "flipflop"` subtype. It exposes the standard digital-logic bistables — T, SR, gated SR, D, D-latch, JK — through a uniform set of "wire" attributes. Each declared wire is a boolean HCL expression observed reactively; combinations of wires name the variant.
The flipflop resolves its wires to a single desired output value each cycle (applying the conflict-resolution priority: gate → set/reset dominance → set/reset over toggle → D-sample over toggle) and feeds that value to the shared StateMachine via SetRawInput. The StateMachine owns latch / invert / cooldown / inhibit and the four-state output model, so those behaviors carry over identically from timer. The flipflop deliberately does NOT use the temporal StateMachine behaviors (activate_after / deactivate_after / timeout / retentive): a flipflop responds to its inputs immediately.
Edge detection is per-wire: each wire stores its previous boolean and a hasPrev flag; the first evaluation (at Start, or the first notification) establishes the baseline and reports no edge, so a source that happens to be asserting at boot does not spuriously drive the flipflop. start_active is the explicit boot-output knob.
Dispatch: rather than rely on the source identity reported by OnChange (which for condition sources is the inner StateMachine, not the capsule a wire referenced — see flipflopSource), the flipflop subscribes one flipflopSource watcher per referenced Watchable, each carrying the wires that reference it. A notification thus re-evaluates only the wires fed by the source that fired; the gate's level is cached so non-gate notifications never re-evaluate it.
func (*FlipflopCondition) Clear ¶ added in v0.41.0
func (f *FlipflopCondition) Clear(ctx context.Context) error
Clear implements richcty.Clearable. Resets the desired output to false and resets the state machine (releasing any latch, restarting cooldown). The wires keep their edge baselines, so the next genuine edge re-drives output.
func (*FlipflopCondition) PostStart ¶ added in v0.41.0
func (f *FlipflopCondition) PostStart() error
PostStart fires the on_init hook after all Startables have bootstrapped.
func (*FlipflopCondition) Set ¶ added in v0.41.0
Set implements richcty.Settable. Unlike timer (which rejects set() once an input= is declared), a flipflop accepts imperative set() even with wires declared — the wires are additional event sources, not the only sources. Honors latch / inhibit / cooldown via the normal SetRawInput path (e.g. set(false) is ignored while latched-active).
func (*FlipflopCondition) Start ¶ added in v0.41.0
func (f *FlipflopCondition) Start() error
Start bootstraps the state machine, establishes the per-wire baselines without firing any edge (including the cached gate assertion), starts the inhibit expression, and subscribes each source. No synthetic boot transition is produced — start_active is the only boot-output knob.
func (*FlipflopCondition) State ¶ added in v0.41.0
func (f *FlipflopCondition) State(ctx context.Context) (string, error)
func (*FlipflopCondition) Stop ¶ added in v0.41.0
func (f *FlipflopCondition) Stop() error
Stop unsubscribes every source and stops the inhibit expression.
func (*FlipflopCondition) Toggle ¶ added in v0.41.0
Toggle implements richcty.Toggleable. Equivalent to set(condition.x, !current), flipping the desired output directly. Always permitted. Returns the new value.
func (*FlipflopCondition) Unwatch ¶ added in v0.41.0
func (f *FlipflopCondition) Unwatch(w richcty.Watcher)
func (*FlipflopCondition) Watch ¶ added in v0.41.0
func (f *FlipflopCondition) Watch(w richcty.Watcher)
type HookDispatcher ¶ added in v0.35.0
type HookDispatcher struct {
// contains filtered or unexported fields
}
HookDispatcher evaluates condition lifecycle hooks. It is registered as an internal Watcher on the condition's StateMachine so on_activate / on_deactivate fire synchronously on every output transition; on_init fires once from the subtype's PostStart().
Error handling: any diagnostics from building the eval context or evaluating the hook expression are logged to UserLogger and do not propagate — matches the existing trigger "watch" behaviour.
Context propagation: OnChange receives the ctx from sm.notifyAll(ctx, ...). That ctx carries any caller trace span (e.g. from an inbound message that drove the transition) or is context.Background() for autonomous timer- driven transitions; either way the hook opens a child/root span named "trigger.condition.<hook> <name>".
func NewHookDispatcher ¶ added in v0.35.0
func NewHookDispatcher(name string, hooks Hooks, config *cfg.Config, tp trace.TracerProvider) *HookDispatcher
NewHookDispatcher constructs a dispatcher. Returns nil when no hooks are configured so callers can skip the Watch() registration entirely.
func (*HookDispatcher) FireInit ¶ added in v0.35.0
func (h *HookDispatcher) FireInit(currentOutput bool)
FireInit evaluates on_init once. Called from the owning subtype's PostStart() with the condition's current output.
type Hooks ¶ added in v0.35.0
type Hooks struct {
OnInit hcl.Expression
OnActivate hcl.Expression
OnDeactivate hcl.Expression
}
Hooks bundles the three condition lifecycle action expressions. Zero-valued (nil) expressions are treated as "not configured" and skipped. Kept as a shared value type even though gohcl cannot inline-promote its fields into subtype body structs — the runtime plumbing (HookDispatcher) is shared.
type StateMachine ¶
type StateMachine struct {
richcty.WatchableMixin
// contains filtered or unexported fields
}
StateMachine implements the four-state output logic shared by every condition subtype. It consumes a single boolean "raw" input signal (already filtered by the subtype — e.g. through debounce for timer, hysteresis for threshold, preset comparison for counter) and exposes the conditioned output with temporal semantics layered on top.
StateMachine is Watchable (fires on output transitions only — never on pending-state transitions), Gettable (returns the output bool, inverted if configured), Stateful (returns the internal four-state name), and Clearable (cancels any pending state / releases any latch).
Zero-valued Behavior fields disable their respective behaviors; the StateMachine with a fully-zeroed Behavior simply tracks the raw input one-to-one.
func NewStateMachine ¶
func NewStateMachine(behavior Behavior, clock Clock) *StateMachine
NewStateMachine creates a StateMachine with the given behavior. If clock is nil, RealClock is used.
func (*StateMachine) Bootstrap ¶ added in v0.35.0
func (sm *StateMachine) Bootstrap()
Bootstrap applies the configured initial state. Must be called exactly once, from the owning subtype's Start() method, before any input is pushed to the state machine. If behavior.StartActive is false, this is a no-op.
When StartActive is true: state is forced to Active. If behavior.Latch is also true, the latch is engaged (only clear()/reset() can release it). If Latch is false and Timeout > 0, the timeout timer is armed. No NotifyAll fires — boot-active is a silent initial condition, not a synthetic rising edge.
Bootstrap does NOT consult inhibit: inhibit prevents new activations, and a configured initial state is not an activation. Invert is applied by Output() as usual, so start_active + invert produces external get() == false.
func (*StateMachine) Clear ¶
func (sm *StateMachine) Clear(ctx context.Context) error
Clear implements richcty.Clearable. Resets the state machine to Inactive, cancels any pending state, releases any latch, and (for subtypes backing them onto this machine) stops timers. Safe to call in any state.
func (*StateMachine) Get ¶
Get implements richcty.Gettable. The optional default argument is ignored: a condition always has a defined output.
func (*StateMachine) Output ¶
func (sm *StateMachine) Output() bool
Output returns the current boolean output (post-inversion). Safe for concurrent use.
func (*StateMachine) RawInput ¶ added in v0.35.0
func (sm *StateMachine) RawInput() bool
RawInput returns the most recent value tracked by the state machine. Subtype wrappers use this to reconcile their own input cache after the state machine has auto-changed its view of the input (e.g. via timeout-driven deactivation or Clear()).
func (*StateMachine) SetInhibited ¶
func (sm *StateMachine) SetInhibited(ctx context.Context, inhibited bool)
SetInhibited updates the inhibit gate. When inhibit is asserted and the machine is currently in pending_activation, the pending transition is cancelled and (for retentive timers) the accumulator is discarded. Active and pending_deactivation states are unaffected per spec: inhibit prevents new activations, it does not force deactivation. When inhibit clears while the underlying input is still asserted, activation resumes from scratch (activate_after restarts; retentive accumulator is already zeroed).
func (*StateMachine) SetRawInput ¶
func (sm *StateMachine) SetRawInput(ctx context.Context, value bool)
SetRawInput drives the state machine from the subtype's pre-filtered boolean input signal. ctx is forwarded to watcher notifications.
type ThresholdCondition ¶
type ThresholdCondition struct {
// contains filtered or unexported fields
}
ThresholdCondition is the `condition "threshold"` subtype. It derives a boolean from a numeric input using separate on/off thresholds (hysteresis) and drives the shared StateMachine with that derived boolean — so activate_after / deactivate_after / timeout / cooldown / latch / invert / retentive semantics carry over identically from timer. `input =` is required (numeric, reactive only); there is no imperative set().
func (*ThresholdCondition) Clear ¶
func (c *ThresholdCondition) Clear(ctx context.Context) error
Clear resets the hysteresis state machine and cancels any in-flight debounce. The declared input is re-sampled afterward and hysteresis is applied from the freshly-reset baseline (derived=false): a value above the on threshold re-activates the condition; values inside the deadband leave it inactive (consistent with the first-sample initial-value rule). When re-activation occurs and latch is configured, the latch re-engages automatically — clear() releases the latch but does not silence an ongoing-true input. Debounce is bypassed on the re-activation edge since the signal has already proven stable.
func (*ThresholdCondition) PostStart ¶ added in v0.35.0
func (c *ThresholdCondition) PostStart() error
PostStart fires the on_init hook after all Startables have bootstrapped. Implements config.PostStartable.
func (*ThresholdCondition) Start ¶
func (c *ThresholdCondition) Start() error
func (*ThresholdCondition) State ¶
func (c *ThresholdCondition) State(ctx context.Context) (string, error)
func (*ThresholdCondition) Stop ¶
func (c *ThresholdCondition) Stop() error
func (*ThresholdCondition) Unwatch ¶
func (c *ThresholdCondition) Unwatch(w richcty.Watcher)
func (*ThresholdCondition) Watch ¶
func (c *ThresholdCondition) Watch(w richcty.Watcher)
type TimerCondition ¶
type TimerCondition struct {
// contains filtered or unexported fields
}
TimerCondition is the `condition "timer"` subtype. It takes a boolean input (either supplied imperatively via set() or declared reactively via input=), applies optional debounce pre-filtering, and drives a StateMachine that owns the temporal semantics (activate_after / deactivate_after / timeout / cooldown / latch / invert / retentive).
func (*TimerCondition) Clear ¶
func (t *TimerCondition) Clear(ctx context.Context) error
Clear implements richcty.Clearable. In addition to resetting the state machine (cancelling pending state, releasing latch, discarding retentive accumulation), it cancels any in-flight debounce.
When a declared input expression is currently truthy, the rising edge is re-asserted into the state machine so the condition re-activates (and, if latch is configured, re-latches): clear() releases the latch but does not silence an ongoing-true input. Debounce is bypassed — the signal has already proven stable through the debounce window. activate_after, cooldown, and inhibit gating still apply via the normal SetRawInput path.
func (*TimerCondition) PostStart ¶ added in v0.35.0
func (t *TimerCondition) PostStart() error
PostStart fires the on_init hook after all Startables have bootstrapped. Implements config.PostStartable. Called by the config runtime exactly once, in Startables order, after the Startables phase completes. No-op when no hooks were configured (hooks is nil and the subtype was not added to PostStartables in that case, so this method should not be invoked — the nil-guard in FireInit makes it safe regardless).
func (*TimerCondition) Set ¶
Set implements richcty.Settable. Rejects calls when a declarative input= was configured (per spec §Functions).
func (*TimerCondition) Start ¶
func (t *TimerCondition) Start() error
Start is invoked from the config Startables phase. It wires up the reactive input and inhibit expressions; each subscribes to its Watchables and pushes initial values into the condition.
func (*TimerCondition) State ¶
func (t *TimerCondition) State(ctx context.Context) (string, error)
State implements richcty.Stateful.
func (*TimerCondition) Stop ¶
func (t *TimerCondition) Stop() error
Stop unsubscribes reactive expressions so the condition stops reacting to upstream changes during shutdown.
func (*TimerCondition) Toggle ¶ added in v0.37.0
Toggle implements richcty.Toggleable. Equivalent to set(condition.x, !current), flipping the bistable's input through the normal debounce + state-machine path. Rejected when a declared input= expression is present (the condition is then level-tracking, not bistable — same rule as Set). Extra args are ignored. Returns the new input boolean.
The basis for the flip is the post-debounce stableInput, reconciled against the state machine's RawInput() first to pick up auto-resets from timeout or Clear() (mirrors the reconcile in submitInput). All gating — activate_after, deactivate_after, cooldown, latch, inhibit, invert — applies as for any other input edge.
func (*TimerCondition) Unwatch ¶
func (t *TimerCondition) Unwatch(w richcty.Watcher)
func (*TimerCondition) Watch ¶
func (t *TimerCondition) Watch(w richcty.Watcher)
Watch implements richcty.Watchable by forwarding to the state machine, whose watcher notifications already respect pending-state suppression and invert.