1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
//! Events the kernel emits on the edge where a reconciled fact changed.
//!
//! Every event here fires on a change edge and never on a settled frame, so a consumer can treat
//! one arrival as one transition instead of debouncing a per-frame restatement.
//!
//! # Which events exist, and why
//!
//! The list is derived from the state axes the kernel mirrors onto an entity, not accumulated one
//! case at a time: `crate::Presence`, `crate::Claim`, `crate::IdentityVerdict`,
//! `crate::RecoveryPolicy`, `crate::RoleState`, and attempt completion each have exactly one
//! event. A mirrored axis with no event, or an event with no mirrored axis, is the defect this
//! derivation exists to prevent.
//!
//! Each transition event carries only the state moved *to*. The state moved *from* is still on the
//! entity when an observer runs, so duplicating it in the payload would let the two disagree.
//!
//! # Two axes deliberately outside the derivation
//!
//! `crate::WaitingWork` is not on the binding entity and `crate::IdentityDecisionOwed` is not on
//! the device entity, so neither is mirrored. A consumer reading only entities or the Bevy Remote
//! Protocol therefore cannot see that a role owes a restoration or that a human owes an identity
//! decision; both are read from the resources instead — `crate::Bindings::waiting_work` and
//! `crate::ReconciledDeviceState::decision_owed`. This is stated rather than left implicit so that
//! surfacing either one is a decision somebody makes, not a hole somebody finds.
//!
//! `IdentityQuestionRaised` and `IdentityQuestionExpired` are that decision, taken for the identity
//! debt and for nothing else. Every other axis the kernel reports is state an application can read
//! whenever it gets around to it, whereas an identity question exists to make a human act, and one
//! that is never noticed leaves a device unusable. A consumer that only polls
//! `crate::IdentityDecisions` cannot see a question that arrived and expired between two reads, and
//! a dialog it opened has no signal that the entry vanished underneath it.
//!
//! # Why some events target an entity and some are global
//!
//! An event is an `EntityEvent` only when an entity is guaranteed to exist to receive it. A role
//! whose device has never appeared, or whose binding was retired in the same frame its attempt
//! ended, has no valid `Entity` to name — so those cases are global `Event`s carrying the durable
//! `crate::DeviceKey` or `crate::RoleKey` instead.
//!
//! Per-unit facts (`crate::Presence`, `crate::Claim`, `crate::IdentityVerdict`) target the device
//! entity; per-role facts (`crate::RoleState`, `crate::RecoveryPolicy`, attempt completion) target
//! the binding entity. The split is not stylistic: a device departure can despawn the device entity
//! while an attempt is still finishing, so an `AttemptFinished` aimed at the device entity would
//! have nowhere to land.
use VecDeque;
use EntityEvent;
use Event;
use Entity;
use Reflect;
use Resource;
use crateAttemptId;
use crateAttemptOutcome;
use crateClaim;
use crateCompletedDiscoveryOutcome;
use crateConfiguredDeviceConnection;
use crateDeviceAccessError;
use crateDeviceId;
use crateDeviceKey;
use crateDeviceRevision;
use crateDiscoveryBatchId;
use crateDiscoveryProgress;
use crateIdentityVerdict;
use cratePresence;
use crateRecoveryPolicy;
use crateReporterId;
use crateRoleKey;
use crateRoleState;
use crateSchemeName;
use crateStartupDiscoveryState;
use crateDeviceDeparture;
/// The capabilities two reporters disagree about for this device changed.
///
/// A co-reported unit whose reporters contradict each other about one capability stays drivable
/// for every capability they agree about, so the disagreement has to reach a diagnostic somehow:
/// nothing else in the kernel reports which capability went contested. Emitted on the change edge
/// only. An empty `capabilities` means the disagreement cleared and the device is fully drivable
/// again.
/// One attempt reached a terminal outcome while its role still had a binding entity.
///
/// Targeted at the binding entity rather than the device entity because the binding outlives the
/// unit: an attempt that ends because the device departed still has somewhere to land. The role is
/// carried alongside the target so a consumer that observes the event does not have to read the
/// entity's components back to learn which role ended.
/// Guarded report that an established local endpoint session ended while its device remained
/// present.
///
/// Integration code submits this value outside [`crate::EndpointDriver`] dispatch. The kernel
/// later compares the establishing attempt and both process-local device fields with the role's
/// current resolution at
/// [`crate::RiggingSystems::SessionLoss`], so a report retained from a replaced or rebound session
/// cannot move its successor.
/// Inbox integrations use to hand established-session failures to the ordered kernel lifecycle.
///
/// Submission only retains a report. It deliberately does not mutate [`crate::Bindings`] or
/// [`crate::Devices`]; [`crate::RiggingPlugin`] drains the inbox at
/// [`crate::RiggingSystems::SessionLoss`] after application preparation and before apply dispatch.
/// Why one submitted session-loss report was refused as stale or inapplicable.
/// Kernel decision made for one guarded established-session failure.
/// Observable result of processing one [`SessionLossReport`].
///
/// This is global rather than entity-targeted because retirement and replacement can remove the
/// binding entity before a stale report reaches the ordered lifecycle point.
/// An attempt ended after its role was retired or replaced, so no binding entity remained.
///
/// Global rather than entity-targeted: retirement despawns the binding entity in the same frame the
/// kernel aborts the attempt, and an event addressed to a despawned entity reaches no observer at
/// all. The ending still has to be reportable, so it carries the `RoleKey` the entity would have
/// identified.
/// An authored role left the kernel's binding set and no longer authorizes its endpoint.
///
/// This is global rather than entity-targeted because applying the retirement despawns the
/// binding entity in the same lifecycle stage. The role and endpoint remain useful to an
/// integration that must retire its own local session or visible representation without looking
/// up an entity that no longer exists.
/// A durable key entered the reconciled device set and now has a device entity behind it.
///
/// This is what lets an integration say "*my* Stream Deck came back" by observing one entity
/// instead of writing a global match arm over every device kind the process reports. It fires once
/// per spawn: a unit that goes absent without its key leaving the set keeps its entity and produces
/// a `PresenceChanged` instead, so a second `DeviceArrived` for the same entity never happens.
/// Reachability for this unit moved to a different `crate::Presence` variant.
///
/// Compared by variant, never by value: `crate::Presence::Unreachable` carries an elapsed time that
/// grows on every scan, so a value comparison would emit this event at scan rate forever and defeat
/// the once-per-change rule the whole module is built on.
/// Exclusive ownership of this unit changed hands, or the permission gating it did.
///
/// Separate from `PresenceChanged` because a camera can be plugged in and fully present while
/// another process owns its capture stream: a consumer that treated the two as one axis would show
/// a contended camera as missing hardware.
/// The kernel reached a different conclusion about whether this unit is the one its key names.
///
/// This is the event a `crate::IdentityVerdict::Displaced` conclusion reaches a consumer through: a
/// unit that moved to the port a departed one occupied is drivable for nothing until a human
/// resolves it, and nothing else reports that the conclusion changed.
/// The kernel added a question to `crate::IdentityDecisions` that only a human can settle.
///
/// A stated exception to the derivation above: `crate::IdentityDecisionOwed` has no mirrored axis,
/// and this event exists because a question nobody notices leaves a device unusable for the life of
/// the process. What an application does with it is its own — a notification that expands into the
/// register, an attention marker on the mesh representing that hardware.
///
/// Global rather than entity-targeted because it names two sides at once: the role's binding entity
/// may not exist while its device is absent, and the candidate's device entity is not what the
/// operator is being asked about.
/// A standing identity question went away without being answered.
///
/// The other half of the stated exception `IdentityQuestionRaised` documents. It fires when the
/// candidate device departs or the role is retired, and never for an entry an answer removed: a
/// dialog the operator is looking at needs to know the question underneath it is gone, and an
/// application that answered already knows.
/// A role's lifecycle state moved.
///
/// Emitted from `crate::RiggingSystems::Apply` beside `AttemptFinished`, not from the entity
/// mirror. The mirror refreshes at the top of `crate::RiggingSystems::Reconcile` while the apply
/// systems write `crate::RoleState` a full set later and can move one role
/// `Applying → Waiting → Applying` inside a single frame; a mirror-derived event would arrive one
/// frame late and collapse both transitions into one, leaving a consumer unable to count attempts
/// from events.
/// The retention rule applied when this role's device departs was re-authored.
///
/// Exists because the once-per-change rule is derived from the mirrored component set and
/// `crate::RecoveryPolicy` is in it; without this event the derivation would have a hole. A user
/// interface that shows what happens to a role on unplug reads it to stay current when application
/// code re-registers the binding with a different policy.
/// Application request to re-apply a role's saved configuration now.
///
/// This is what clears the `crate::WaitingWork::ApplicationRequestOwed` that a departure recorded,
/// and it is the kernel's replacement for clerestory's `RestoreWindow`. It is a *request from* the
/// application, not a report to it: the kernel observes it and answers according to the role's
/// `crate::RecoveryPolicy`.
///
/// It clears the owed request only for `crate::RecoveryPolicy::ReapplyOnRequest`. For
/// `crate::RecoveryPolicy::Retain` the kernel refuses — that policy promises the kernel remembers
/// and reports but never touches the device, and honouring a request here would break the promise
/// through the front door. For `crate::RecoveryPolicy::Forget` it refuses because the saved value
/// was already dropped at the departure and there is nothing left to re-apply.
/// One device stopped being usable, and which of the two ways it stopped by.
///
/// Global rather than entity-targeted because one of the two causes despawns the device entity in
/// the same frame, leaving an entity-addressed event with nowhere to land — and a consumer of that
/// cause has no entity left to read the key back from, which is why the durable
/// `crate::DeviceKey` travels in the payload.
///
/// The cause travels too rather than being discarded: both causes make every
/// `crate::RecoveryPolicy::ReapplyOnReturn` role owe its restoration, but only
/// `crate::DeviceDeparture::KeyLeftTheSet` retires the handle and despawns the entity. A consumer
/// that must tell "unplugged" from "still enumerated but not present" can do it from this payload
/// alone.
/// Application request to retire a role and stop everything the kernel is doing for it.
///
/// Global rather than entity-targeted so an application can retire a role it never saw a binding
/// entity for — a role registered and retired inside one frame has no entity yet. Replaces
/// clerestory's `CancelWindowRecovery`.
/// A registered role has no live device behind its endpoint and is waiting for one.
///
/// Global for the reason the interval itself exists: during it there is no device entity to address
/// and the binding entity may not have been spawned yet. Mirrors clerestory's
/// `WindowRecoveryPending`, and is what a user interface shows a "waiting for display" state from.
/// A registered role's endpoint resolved to a live device again.
///
/// The closing edge of `RoleAwaiting`, and global for the same reason: it is the transition out of
/// the interval where no entity could carry it. Mirrors clerestory's `WindowRecoveryAvailable`.
/// The kernel reached a different conclusion about whether an authored inventory key is connected.
///
/// Global because an authored key with no live unit behind it has no device entity: this is exactly
/// the event that lets a user interface list a configured-but-absent camera without inventing a
/// placeholder entity for it. Once a live unit is identified, the device-targeted events carry its
/// detailed presence, claim, and verdict transitions instead.
/// A reporter named a device under a `crate::SchemeName` no `RiggingAppExt` call registered.
///
/// The record is rejected at the ingest boundary and produces no device, no mirrored component, and
/// therefore no other event — so without this one a typo in a reporter's scheme name is completely
/// silent, and a reporter author debugging a device that never appears has nothing to look at.
/// Fires once per scheme, on the first record rejected under it; the scheme also stays readable
/// from `crate::Devices::unregistered_schemes`.
/// One reporter's running discovery job reported movement, and where that leaves its batch.
///
/// Global because a discovery run belongs to a reporter, not to any device: the run is what
/// decides which devices exist, so at the moment it is running there may be no entity for it to
/// address. Suppressed until the run has been going for `crate::DiscoveryLimits::progress_after`,
/// so a scan that finishes quickly produces no progress traffic and an application does not flash a
/// spinner for a run that was over before a human could read it.
///
/// The reporter's own report and the batch counts ride the same event because they are read from
/// one recorded transition: splitting them into two events made a consumer correlate two callbacks
/// that could never arrive apart, and left the aggregate free to disagree with the report that
/// produced it. A progress indicator reads the four counts, since one reporter's `Measured` count
/// says nothing about whether the application can proceed; a per-reporter view reads `reporter` and
/// `progress`. Neither needs a second observer.
/// One reporter's discovery run reached a terminal outcome and the kernel accepted it.
///
/// Global for the same reason as `DiscoveryProgressChanged`, and carrying
/// `crate::CompletedDiscoveryOutcome` rather than the retained
/// `crate::LastDiscoveryOutcome`: a run that just ended cannot be in the never-completed state, and
/// a consumer should not have to write an arm for a case this event can never carry.
/// The required-before-ready startup gate moved.
///
/// Global because it is a statement about the process rather than about any one device, and it is
/// the edge form of `crate::DiscoveryStatus::startup`: reading that field tells a system what the
/// gate is right now, while an application that wants to show "waiting for displays", "displays
/// failed", or "ready" needs the transition itself.