@sparkleideas/claims 3.5.2-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,779 @@
1
+ /**
2
+ * Claim Domain Events (ADR-007)
3
+ *
4
+ * Domain events for the claims system following event sourcing pattern.
5
+ * All state changes emit events for audit trail and projections.
6
+ *
7
+ * @module v3/claims/domain/events
8
+ */
9
+
10
+ import type {
11
+ ClaimId,
12
+ IssueId,
13
+ Claimant,
14
+ ClaimStatus,
15
+ } from './types.js';
16
+
17
+ // =============================================================================
18
+ // Base Claim Event
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Base interface for all claim domain events
23
+ */
24
+ export interface ClaimDomainEvent {
25
+ /** Unique event identifier */
26
+ id: string;
27
+
28
+ /** Event type discriminator */
29
+ type: ClaimEventType;
30
+
31
+ /** Aggregate ID (claim ID) */
32
+ aggregateId: string;
33
+
34
+ /** Aggregate type - always 'claim' for this domain */
35
+ aggregateType: 'claim';
36
+
37
+ /** Event version for ordering */
38
+ version: number;
39
+
40
+ /** Timestamp when event occurred */
41
+ timestamp: number;
42
+
43
+ /** Event source */
44
+ source: string;
45
+
46
+ /** Event payload data */
47
+ payload: Record<string, unknown>;
48
+
49
+ /** Optional metadata */
50
+ metadata?: Record<string, unknown>;
51
+
52
+ /** Optional causation ID (event that caused this event) */
53
+ causationId?: string;
54
+
55
+ /** Optional correlation ID (groups related events) */
56
+ correlationId?: string;
57
+ }
58
+
59
+ // =============================================================================
60
+ // Event Types
61
+ // =============================================================================
62
+
63
+ export type ClaimEventType =
64
+ | 'claim:created'
65
+ | 'claim:released'
66
+ | 'claim:expired'
67
+ | 'claim:status-changed'
68
+ | 'claim:note-added'
69
+ | 'handoff:requested'
70
+ | 'handoff:accepted'
71
+ | 'handoff:rejected'
72
+ | 'review:requested'
73
+ | 'review:completed';
74
+
75
+ // =============================================================================
76
+ // Specific Event Interfaces
77
+ // =============================================================================
78
+
79
+ export interface ClaimCreatedEvent extends ClaimDomainEvent {
80
+ type: 'claim:created';
81
+ payload: {
82
+ claimId: ClaimId;
83
+ issueId: IssueId;
84
+ claimant: Claimant;
85
+ claimedAt: number;
86
+ expiresAt?: number;
87
+ };
88
+ }
89
+
90
+ export interface ClaimReleasedEvent extends ClaimDomainEvent {
91
+ type: 'claim:released';
92
+ payload: {
93
+ claimId: ClaimId;
94
+ issueId: IssueId;
95
+ claimant: Claimant;
96
+ releasedAt: number;
97
+ reason?: string;
98
+ };
99
+ }
100
+
101
+ export interface ClaimExpiredEvent extends ClaimDomainEvent {
102
+ type: 'claim:expired';
103
+ payload: {
104
+ claimId: ClaimId;
105
+ issueId: IssueId;
106
+ claimant: Claimant;
107
+ expiredAt: number;
108
+ lastActivityAt: number;
109
+ };
110
+ }
111
+
112
+ export interface ClaimStatusChangedEvent extends ClaimDomainEvent {
113
+ type: 'claim:status-changed';
114
+ payload: {
115
+ claimId: ClaimId;
116
+ issueId: IssueId;
117
+ previousStatus: ClaimStatus;
118
+ newStatus: ClaimStatus;
119
+ changedAt: number;
120
+ note?: string;
121
+ };
122
+ }
123
+
124
+ export interface ClaimNoteAddedEvent extends ClaimDomainEvent {
125
+ type: 'claim:note-added';
126
+ payload: {
127
+ claimId: ClaimId;
128
+ issueId: IssueId;
129
+ note: string;
130
+ addedAt: number;
131
+ addedBy: Claimant;
132
+ };
133
+ }
134
+
135
+ export interface HandoffRequestedEvent extends ClaimDomainEvent {
136
+ type: 'handoff:requested';
137
+ payload: {
138
+ claimId: ClaimId;
139
+ issueId: IssueId;
140
+ handoffId: string;
141
+ from: Claimant;
142
+ to: Claimant;
143
+ reason: string;
144
+ requestedAt: number;
145
+ };
146
+ }
147
+
148
+ export interface HandoffAcceptedEvent extends ClaimDomainEvent {
149
+ type: 'handoff:accepted';
150
+ payload: {
151
+ claimId: ClaimId;
152
+ issueId: IssueId;
153
+ handoffId: string;
154
+ from: Claimant;
155
+ to: Claimant;
156
+ acceptedAt: number;
157
+ };
158
+ }
159
+
160
+ export interface HandoffRejectedEvent extends ClaimDomainEvent {
161
+ type: 'handoff:rejected';
162
+ payload: {
163
+ claimId: ClaimId;
164
+ issueId: IssueId;
165
+ handoffId: string;
166
+ from: Claimant;
167
+ to: Claimant;
168
+ rejectedAt: number;
169
+ reason: string;
170
+ };
171
+ }
172
+
173
+ export interface ReviewRequestedEvent extends ClaimDomainEvent {
174
+ type: 'review:requested';
175
+ payload: {
176
+ claimId: ClaimId;
177
+ issueId: IssueId;
178
+ reviewers: Claimant[];
179
+ requestedAt: number;
180
+ requestedBy: Claimant;
181
+ };
182
+ }
183
+
184
+ export interface ReviewCompletedEvent extends ClaimDomainEvent {
185
+ type: 'review:completed';
186
+ payload: {
187
+ claimId: ClaimId;
188
+ issueId: IssueId;
189
+ reviewer: Claimant;
190
+ approved: boolean;
191
+ completedAt: number;
192
+ comments?: string;
193
+ };
194
+ }
195
+
196
+ // =============================================================================
197
+ // Event Type Union
198
+ // =============================================================================
199
+
200
+ export type AllClaimEvents =
201
+ | ClaimCreatedEvent
202
+ | ClaimReleasedEvent
203
+ | ClaimExpiredEvent
204
+ | ClaimStatusChangedEvent
205
+ | ClaimNoteAddedEvent
206
+ | HandoffRequestedEvent
207
+ | HandoffAcceptedEvent
208
+ | HandoffRejectedEvent
209
+ | ReviewRequestedEvent
210
+ | ReviewCompletedEvent;
211
+
212
+ // =============================================================================
213
+ // Event Factory Functions
214
+ // =============================================================================
215
+
216
+ let eventCounter = 0;
217
+
218
+ function generateEventId(): string {
219
+ return `claim-evt-${Date.now()}-${++eventCounter}`;
220
+ }
221
+
222
+ function createClaimEvent<T extends ClaimDomainEvent>(
223
+ type: T['type'],
224
+ aggregateId: string,
225
+ payload: T['payload'],
226
+ metadata?: Record<string, unknown>,
227
+ causationId?: string,
228
+ correlationId?: string
229
+ ): T {
230
+ return {
231
+ id: generateEventId(),
232
+ type,
233
+ aggregateId,
234
+ aggregateType: 'claim',
235
+ version: 1, // Version will be set by event store
236
+ timestamp: Date.now(),
237
+ source: 'claim-service',
238
+ payload,
239
+ metadata,
240
+ causationId,
241
+ correlationId,
242
+ } as T;
243
+ }
244
+
245
+ // =============================================================================
246
+ // Public Event Factory Functions
247
+ // =============================================================================
248
+
249
+ export function createClaimCreatedEvent(
250
+ claimId: ClaimId,
251
+ issueId: IssueId,
252
+ claimant: Claimant,
253
+ expiresAt?: number
254
+ ): ClaimCreatedEvent {
255
+ return createClaimEvent('claim:created', claimId, {
256
+ claimId,
257
+ issueId,
258
+ claimant,
259
+ claimedAt: Date.now(),
260
+ expiresAt,
261
+ });
262
+ }
263
+
264
+ export function createClaimReleasedEvent(
265
+ claimId: ClaimId,
266
+ issueId: IssueId,
267
+ claimant: Claimant,
268
+ reason?: string
269
+ ): ClaimReleasedEvent {
270
+ return createClaimEvent('claim:released', claimId, {
271
+ claimId,
272
+ issueId,
273
+ claimant,
274
+ releasedAt: Date.now(),
275
+ reason,
276
+ });
277
+ }
278
+
279
+ export function createClaimExpiredEvent(
280
+ claimId: ClaimId,
281
+ issueId: IssueId,
282
+ claimant: Claimant,
283
+ lastActivityAt: number
284
+ ): ClaimExpiredEvent {
285
+ return createClaimEvent('claim:expired', claimId, {
286
+ claimId,
287
+ issueId,
288
+ claimant,
289
+ expiredAt: Date.now(),
290
+ lastActivityAt,
291
+ });
292
+ }
293
+
294
+ export function createClaimStatusChangedEvent(
295
+ claimId: ClaimId,
296
+ issueId: IssueId,
297
+ previousStatus: ClaimStatus,
298
+ newStatus: ClaimStatus,
299
+ note?: string
300
+ ): ClaimStatusChangedEvent {
301
+ return createClaimEvent('claim:status-changed', claimId, {
302
+ claimId,
303
+ issueId,
304
+ previousStatus,
305
+ newStatus,
306
+ changedAt: Date.now(),
307
+ note,
308
+ });
309
+ }
310
+
311
+ export function createClaimNoteAddedEvent(
312
+ claimId: ClaimId,
313
+ issueId: IssueId,
314
+ note: string,
315
+ addedBy: Claimant
316
+ ): ClaimNoteAddedEvent {
317
+ return createClaimEvent('claim:note-added', claimId, {
318
+ claimId,
319
+ issueId,
320
+ note,
321
+ addedAt: Date.now(),
322
+ addedBy,
323
+ });
324
+ }
325
+
326
+ export function createHandoffRequestedEvent(
327
+ claimId: ClaimId,
328
+ issueId: IssueId,
329
+ handoffId: string,
330
+ from: Claimant,
331
+ to: Claimant,
332
+ reason: string
333
+ ): HandoffRequestedEvent {
334
+ return createClaimEvent('handoff:requested', claimId, {
335
+ claimId,
336
+ issueId,
337
+ handoffId,
338
+ from,
339
+ to,
340
+ reason,
341
+ requestedAt: Date.now(),
342
+ });
343
+ }
344
+
345
+ export function createHandoffAcceptedEvent(
346
+ claimId: ClaimId,
347
+ issueId: IssueId,
348
+ handoffId: string,
349
+ from: Claimant,
350
+ to: Claimant
351
+ ): HandoffAcceptedEvent {
352
+ return createClaimEvent('handoff:accepted', claimId, {
353
+ claimId,
354
+ issueId,
355
+ handoffId,
356
+ from,
357
+ to,
358
+ acceptedAt: Date.now(),
359
+ });
360
+ }
361
+
362
+ export function createHandoffRejectedEvent(
363
+ claimId: ClaimId,
364
+ issueId: IssueId,
365
+ handoffId: string,
366
+ from: Claimant,
367
+ to: Claimant,
368
+ reason: string
369
+ ): HandoffRejectedEvent {
370
+ return createClaimEvent('handoff:rejected', claimId, {
371
+ claimId,
372
+ issueId,
373
+ handoffId,
374
+ from,
375
+ to,
376
+ rejectedAt: Date.now(),
377
+ reason,
378
+ });
379
+ }
380
+
381
+ export function createReviewRequestedEvent(
382
+ claimId: ClaimId,
383
+ issueId: IssueId,
384
+ reviewers: Claimant[],
385
+ requestedBy: Claimant
386
+ ): ReviewRequestedEvent {
387
+ return createClaimEvent('review:requested', claimId, {
388
+ claimId,
389
+ issueId,
390
+ reviewers,
391
+ requestedAt: Date.now(),
392
+ requestedBy,
393
+ });
394
+ }
395
+
396
+ export function createReviewCompletedEvent(
397
+ claimId: ClaimId,
398
+ issueId: IssueId,
399
+ reviewer: Claimant,
400
+ approved: boolean,
401
+ comments?: string
402
+ ): ReviewCompletedEvent {
403
+ return createClaimEvent('review:completed', claimId, {
404
+ claimId,
405
+ issueId,
406
+ reviewer,
407
+ approved,
408
+ completedAt: Date.now(),
409
+ comments,
410
+ });
411
+ }
412
+
413
+ // =============================================================================
414
+ // ADR-016 Extended Events
415
+ // =============================================================================
416
+
417
+ import type {
418
+ AgentId,
419
+ StealReason,
420
+ AgentLoadInfo,
421
+ ClaimMove,
422
+ } from './types.js';
423
+
424
+ /**
425
+ * Extended event types for ADR-016
426
+ */
427
+ export type ExtendedClaimEventType =
428
+ | ClaimEventType
429
+ // Work stealing events
430
+ | 'steal:issue-marked-stealable'
431
+ | 'steal:issue-stolen'
432
+ | 'steal:contest-started'
433
+ | 'steal:contest-resolved'
434
+ | 'steal:warning-sent'
435
+ // Load balancing events
436
+ | 'swarm:rebalanced'
437
+ | 'agent:overloaded'
438
+ | 'agent:underloaded'
439
+ | 'agent:load-changed';
440
+
441
+ /**
442
+ * Extended base event interface for ADR-016 events
443
+ */
444
+ export interface ExtendedClaimDomainEvent {
445
+ /** Unique event identifier */
446
+ id: string;
447
+
448
+ /** Event type discriminator */
449
+ type: ExtendedClaimEventType;
450
+
451
+ /** Aggregate ID (claim ID) */
452
+ aggregateId: string;
453
+
454
+ /** Aggregate type - always 'claim' for this domain */
455
+ aggregateType: 'claim';
456
+
457
+ /** Event version for ordering */
458
+ version: number;
459
+
460
+ /** Timestamp when event occurred */
461
+ timestamp: number;
462
+
463
+ /** Event source */
464
+ source: string;
465
+
466
+ /** Event payload data */
467
+ payload: Record<string, unknown>;
468
+
469
+ /** Optional metadata */
470
+ metadata?: Record<string, unknown>;
471
+
472
+ /** Optional causation ID (event that caused this event) */
473
+ causationId?: string;
474
+
475
+ /** Optional correlation ID (groups related events) */
476
+ correlationId?: string;
477
+ }
478
+
479
+ // =============================================================================
480
+ // Work Stealing Events (ADR-016)
481
+ // =============================================================================
482
+
483
+ export interface IssueMarkedStealableEvent extends ExtendedClaimDomainEvent {
484
+ type: 'steal:issue-marked-stealable';
485
+ payload: {
486
+ claimId: ClaimId;
487
+ issueId: IssueId;
488
+ originalClaimant: Claimant;
489
+ reason: StealReason;
490
+ gracePeriodMs: number;
491
+ gracePeriodEndsAt: number;
492
+ minPriorityToSteal: string;
493
+ requiresContest: boolean;
494
+ };
495
+ }
496
+
497
+ export interface IssueStolenEvent extends ExtendedClaimDomainEvent {
498
+ type: 'steal:issue-stolen';
499
+ payload: {
500
+ claimId: ClaimId;
501
+ issueId: IssueId;
502
+ originalClaimant: Claimant;
503
+ newClaimant: Claimant;
504
+ reason: StealReason;
505
+ hadContest: boolean;
506
+ contestId?: string;
507
+ progressTransferred: number;
508
+ };
509
+ }
510
+
511
+ export interface StealContestStartedEvent extends ExtendedClaimDomainEvent {
512
+ type: 'steal:contest-started';
513
+ payload: {
514
+ contestId: string;
515
+ claimId: ClaimId;
516
+ issueId: IssueId;
517
+ defender: Claimant;
518
+ challenger: Claimant;
519
+ reason: StealReason;
520
+ endsAt: number;
521
+ };
522
+ }
523
+
524
+ export interface StealContestResolvedExtEvent extends ExtendedClaimDomainEvent {
525
+ type: 'steal:contest-resolved';
526
+ payload: {
527
+ contestId: string;
528
+ claimId: ClaimId;
529
+ issueId: IssueId;
530
+ winner: 'defender' | 'challenger';
531
+ winnerClaimant: Claimant;
532
+ loserClaimant: Claimant;
533
+ resolvedBy: AgentId | 'system';
534
+ reason: string;
535
+ };
536
+ }
537
+
538
+ export interface StealWarningEvent extends ExtendedClaimDomainEvent {
539
+ type: 'steal:warning-sent';
540
+ payload: {
541
+ claimId: ClaimId;
542
+ issueId: IssueId;
543
+ claimant: Claimant;
544
+ reason: StealReason;
545
+ warningNumber: number;
546
+ maxWarnings: number;
547
+ stealableAt: number;
548
+ actionRequired: string;
549
+ };
550
+ }
551
+
552
+ // =============================================================================
553
+ // Load Balancing Events (ADR-016)
554
+ // =============================================================================
555
+
556
+ export interface SwarmRebalancedExtEvent extends ExtendedClaimDomainEvent {
557
+ type: 'swarm:rebalanced';
558
+ payload: {
559
+ claimsMoved: number;
560
+ moves: ClaimMove[];
561
+ loadBefore: AgentLoadInfo[];
562
+ loadAfter: AgentLoadInfo[];
563
+ durationMs: number;
564
+ trigger: 'scheduled' | 'overload-detected' | 'underload-detected' | 'manual' | 'agent-added' | 'agent-removed';
565
+ errors: string[];
566
+ };
567
+ }
568
+
569
+ export interface AgentOverloadedExtEvent extends ExtendedClaimDomainEvent {
570
+ type: 'agent:overloaded';
571
+ payload: {
572
+ agentId: AgentId;
573
+ agentName: string;
574
+ currentLoad: number;
575
+ threshold: number;
576
+ activeClaims: number;
577
+ maxClaims: number;
578
+ recommendedAction: 'pause-assignments' | 'rebalance' | 'scale-up' | 'notify-admin';
579
+ };
580
+ }
581
+
582
+ export interface AgentUnderloadedExtEvent extends ExtendedClaimDomainEvent {
583
+ type: 'agent:underloaded';
584
+ payload: {
585
+ agentId: AgentId;
586
+ agentName: string;
587
+ currentLoad: number;
588
+ threshold: number;
589
+ activeClaims: number;
590
+ maxClaims: number;
591
+ availableCapacity: number;
592
+ };
593
+ }
594
+
595
+ export interface AgentLoadChangedEvent extends ExtendedClaimDomainEvent {
596
+ type: 'agent:load-changed';
597
+ payload: {
598
+ agentId: AgentId;
599
+ previousLoad: number;
600
+ currentLoad: number;
601
+ previousClaims: number;
602
+ currentClaims: number;
603
+ changeReason: 'claim-added' | 'claim-completed' | 'claim-released' | 'claim-transferred' | 'capacity-changed';
604
+ };
605
+ }
606
+
607
+ /**
608
+ * All ADR-016 extended events union
609
+ */
610
+ export type AllExtendedClaimEvents =
611
+ | AllClaimEvents
612
+ | IssueMarkedStealableEvent
613
+ | IssueStolenEvent
614
+ | StealContestStartedEvent
615
+ | StealContestResolvedExtEvent
616
+ | StealWarningEvent
617
+ | SwarmRebalancedExtEvent
618
+ | AgentOverloadedExtEvent
619
+ | AgentUnderloadedExtEvent
620
+ | AgentLoadChangedEvent;
621
+
622
+ // =============================================================================
623
+ // Extended Event Factory Functions
624
+ // =============================================================================
625
+
626
+ export function createIssueMarkedStealableEvent(
627
+ claimId: ClaimId,
628
+ issueId: IssueId,
629
+ originalClaimant: Claimant,
630
+ reason: StealReason,
631
+ gracePeriodMs: number,
632
+ minPriorityToSteal: string,
633
+ requiresContest: boolean
634
+ ): IssueMarkedStealableEvent {
635
+ const now = Date.now();
636
+ return {
637
+ id: generateExtEventId(),
638
+ type: 'steal:issue-marked-stealable',
639
+ aggregateId: claimId,
640
+ aggregateType: 'claim',
641
+ version: 1,
642
+ timestamp: now,
643
+ source: 'work-stealing-service',
644
+ payload: {
645
+ claimId,
646
+ issueId,
647
+ originalClaimant,
648
+ reason,
649
+ gracePeriodMs,
650
+ gracePeriodEndsAt: now + gracePeriodMs,
651
+ minPriorityToSteal,
652
+ requiresContest,
653
+ },
654
+ };
655
+ }
656
+
657
+ export function createIssueStolenExtEvent(
658
+ claimId: ClaimId,
659
+ issueId: IssueId,
660
+ originalClaimant: Claimant,
661
+ newClaimant: Claimant,
662
+ reason: StealReason,
663
+ hadContest: boolean,
664
+ progressTransferred: number,
665
+ contestId?: string
666
+ ): IssueStolenEvent {
667
+ return {
668
+ id: generateExtEventId(),
669
+ type: 'steal:issue-stolen',
670
+ aggregateId: claimId,
671
+ aggregateType: 'claim',
672
+ version: 1,
673
+ timestamp: Date.now(),
674
+ source: 'work-stealing-service',
675
+ payload: {
676
+ claimId,
677
+ issueId,
678
+ originalClaimant,
679
+ newClaimant,
680
+ reason,
681
+ hadContest,
682
+ contestId,
683
+ progressTransferred,
684
+ },
685
+ };
686
+ }
687
+
688
+ export function createSwarmRebalancedExtEvent(
689
+ claimsMoved: number,
690
+ moves: ClaimMove[],
691
+ loadBefore: AgentLoadInfo[],
692
+ loadAfter: AgentLoadInfo[],
693
+ durationMs: number,
694
+ trigger: SwarmRebalancedExtEvent['payload']['trigger'],
695
+ errors: string[] = []
696
+ ): SwarmRebalancedExtEvent {
697
+ return {
698
+ id: generateExtEventId(),
699
+ type: 'swarm:rebalanced',
700
+ aggregateId: 'swarm',
701
+ aggregateType: 'claim',
702
+ version: 1,
703
+ timestamp: Date.now(),
704
+ source: 'load-balancer',
705
+ payload: {
706
+ claimsMoved,
707
+ moves,
708
+ loadBefore,
709
+ loadAfter,
710
+ durationMs,
711
+ trigger,
712
+ errors,
713
+ },
714
+ };
715
+ }
716
+
717
+ export function createAgentOverloadedExtEvent(
718
+ agentId: AgentId,
719
+ agentName: string,
720
+ currentLoad: number,
721
+ threshold: number,
722
+ activeClaims: number,
723
+ maxClaims: number,
724
+ recommendedAction: AgentOverloadedExtEvent['payload']['recommendedAction']
725
+ ): AgentOverloadedExtEvent {
726
+ return {
727
+ id: generateExtEventId(),
728
+ type: 'agent:overloaded',
729
+ aggregateId: agentId,
730
+ aggregateType: 'claim',
731
+ version: 1,
732
+ timestamp: Date.now(),
733
+ source: 'load-balancer',
734
+ payload: {
735
+ agentId,
736
+ agentName,
737
+ currentLoad,
738
+ threshold,
739
+ activeClaims,
740
+ maxClaims,
741
+ recommendedAction,
742
+ },
743
+ };
744
+ }
745
+
746
+ export function createAgentUnderloadedExtEvent(
747
+ agentId: AgentId,
748
+ agentName: string,
749
+ currentLoad: number,
750
+ threshold: number,
751
+ activeClaims: number,
752
+ maxClaims: number,
753
+ availableCapacity: number
754
+ ): AgentUnderloadedExtEvent {
755
+ return {
756
+ id: generateExtEventId(),
757
+ type: 'agent:underloaded',
758
+ aggregateId: agentId,
759
+ aggregateType: 'claim',
760
+ version: 1,
761
+ timestamp: Date.now(),
762
+ source: 'load-balancer',
763
+ payload: {
764
+ agentId,
765
+ agentName,
766
+ currentLoad,
767
+ threshold,
768
+ activeClaims,
769
+ maxClaims,
770
+ availableCapacity,
771
+ },
772
+ };
773
+ }
774
+
775
+ let extEventCounter = 0;
776
+
777
+ function generateExtEventId(): string {
778
+ return `claim-ext-evt-${Date.now()}-${++extEventCounter}`;
779
+ }