batpak 0.2.0

Event sourcing with causal graphs and policy gates. Sync API, zero async.
Documentation
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
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
//! Unit tests for the event domain: EventId, EventKind, EventHeader, Event methods,
//! DagPosition, and the define_entity_id! macro.
//! [SPEC:tests/event_api.rs]
//!
//! PROVES: LAW-003 (No Orphan Infrastructure — every pub item exercised)
//! DEFENDS: FM-007 (Island Syndrome — pub items must connect to tests)
//! INVARIANTS: INV-TYPE (event round-trip fidelity), INV-OBS (every pub API has a test witness)

use batpak::id::EntityIdType;
use batpak::prelude::*;

// ================================================================
// src/id/mod.rs — EntityIdType + EventId + define_entity_id! macro
// ================================================================

#[test]
fn event_id_now_v7_is_nonzero() {
    let id = batpak::id::EventId::now_v7();
    assert_ne!(
        id.as_u128(),
        0,
        "PROPERTY: EventId::now_v7() must generate a non-zero UUIDv7.\n\
         Investigate: src/id/mod.rs generate_v7_id().\n\
         Common causes: UUID library returning nil on clock skew, feature flag disabled, \
         or SystemTime before Unix epoch on the test host.\n\
         Run: cargo test --test event_api event_id_now_v7_is_nonzero"
    );
}

#[test]
fn event_id_nil_is_zero() {
    let id = batpak::id::EventId::nil();
    assert_eq!(
        id.as_u128(),
        0,
        "PROPERTY: EventId::nil() must return the zero UUID.\n\
         Investigate: src/id/mod.rs nil().\n\
         Common causes: nil() forwarding to now_v7() by mistake, or inner type default \
         not being zero-initialized.\n\
         Run: cargo test --test event_api event_id_nil_is_zero"
    );
}

#[test]
fn event_id_round_trip() {
    let raw: u128 = 0xDEAD_BEEF_CAFE_BABE_1234_5678_9ABC_DEF0;
    let id = batpak::id::EventId::new(raw);
    assert_eq!(
        id.as_u128(),
        raw,
        "PROPERTY: EventId::new(raw).as_u128() must equal raw (lossless round-trip).\n\
         Investigate: src/id/mod.rs new() as_u128().\n\
         Common causes: byte-order swap in new() or as_u128(), truncation of high bits, \
         or wrapping newtype that strips the value.\n\
         Run: cargo test --test event_api event_id_round_trip"
    );
}

#[test]
fn event_id_display_format() {
    let id = batpak::id::EventId::new(0xFF);
    let s = format!("{id}");
    assert!(
        s.starts_with("event:"),
        "PROPERTY: EventId Display must start with the entity prefix 'event:'.\n\
         Investigate: src/id/mod.rs define_entity_id! Display impl.\n\
         Common causes: macro emitting the wrong prefix string literal, or Display \
         delegating to as_u128() without prepending the prefix.\n\
         Run: cargo test --test event_api event_id_display_format"
    );
    assert!(
        s.contains("ff"),
        "PROPERTY: EventId Display must contain the hex digits of the underlying u128.\n\
         Investigate: src/id/mod.rs define_entity_id! Display impl.\n\
         Common causes: Display printing decimal instead of hex, or padding zeroing \
         out the only non-zero byte before formatting.\n\
         Run: cargo test --test event_api event_id_display_format"
    );
}

#[test]
fn event_id_from_str_with_prefix() {
    use std::str::FromStr;
    let id = batpak::id::EventId::from_str("event:00000000000000000000000000000042")
        .expect("parse with prefix");
    assert_eq!(
        id.as_u128(),
        0x42,
        "PROPERTY: EventId::from_str with 'event:' prefix must parse the hex portion correctly.\n\
         Investigate: src/id/mod.rs define_entity_id! FromStr impl.\n\
         Common causes: prefix stripping off-by-one consuming a hex digit, or \
         from_str not recognising the 'entity:' prefix at all.\n\
         Run: cargo test --test event_api event_id_from_str_with_prefix"
    );
}

#[test]
fn event_id_from_str_bare_hex() {
    use std::str::FromStr;
    let id =
        batpak::id::EventId::from_str("00000000000000000000000000000042").expect("parse bare hex");
    assert_eq!(
        id.as_u128(),
        0x42,
        "PROPERTY: EventId::from_str must parse bare hex (no prefix) correctly.\n\
         Investigate: src/id/mod.rs define_entity_id! FromStr impl.\n\
         Common causes: parser requiring the 'entity:' prefix and returning Err on bare \
         hex, or u128::from_str_radix receiving the wrong slice.\n\
         Run: cargo test --test event_api event_id_from_str_bare_hex"
    );
}

#[test]
fn event_id_from_str_rejects_garbage() {
    use std::str::FromStr;
    let result = batpak::id::EventId::from_str("not_hex_at_all");
    assert!(
        result.is_err(),
        "PROPERTY: EventId::FromStr must reject non-hex input with Err.\n\
         Investigate: src/id/mod.rs define_entity_id! FromStr impl.\n\
         Common causes: parser returning Ok(0) on parse failure instead of Err, \
         or unwrap_or silently swallowing the error.\n\
         Run: cargo test --test event_api event_id_from_str_rejects_garbage"
    );
}

#[test]
fn define_entity_id_custom_type() {
    batpak::define_entity_id!(OrderId, "order");
    use batpak::id::EntityIdType;

    let id = OrderId::now_v7();
    assert_ne!(
        id.as_u128(),
        0,
        "PROPERTY: define_entity_id! macro must generate a non-zero ID via now_v7().\n\
         Investigate: src/id/mod.rs define_entity_id! macro expansion.\n\
         Common causes: macro forwarding to a stub that returns nil, or UUID clock \
         returning zero on test host.\n\
         Run: cargo test --test event_api define_entity_id_custom_type"
    );
    assert_eq!(
        OrderId::ENTITY_NAME,
        "order",
        "PROPERTY: define_entity_id! macro must set ENTITY_NAME to the supplied string.\n\
         Investigate: src/id/mod.rs define_entity_id! macro ENTITY_NAME const.\n\
         Common causes: macro hardcoding a different literal, or const not being \
         set from the macro argument.\n\
         Run: cargo test --test event_api define_entity_id_custom_type"
    );

    let display = format!("{id}");
    assert!(
        display.starts_with("order:"),
        "PROPERTY: Custom entity ID Display must use the registered entity name as prefix.\n\
         Investigate: src/id/mod.rs define_entity_id! Display impl.\n\
         Common causes: Display impl using a hardcoded prefix instead of ENTITY_NAME, \
         or macro not emitting a Display impl at all.\n\
         Run: cargo test --test event_api define_entity_id_custom_type"
    );
}

// ================================================================
// src/event/header.rs — Flag system
// ================================================================

#[test]
fn event_header_flags_requires_ack() {
    let header =
        EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA).with_flags(0x01);
    assert!(
        header.requires_ack(),
        "PROPERTY: Flag bit 0x01 must set requires_ack() to true.\n\
         Investigate: src/event/header.rs requires_ack() flag mask.\n\
         Common causes: requires_ack() testing bit 0x02 instead of 0x01, or \
         with_flags() not storing the value in the flags field.\n\
         Run: cargo test --test event_api event_header_flags_requires_ack"
    );
    assert!(
        !header.is_transactional(),
        "PROPERTY: Flag bit 0x01 must NOT set is_transactional().\n\
         Investigate: src/event/header.rs is_transactional() flag mask.\n\
         Common causes: is_transactional() using the same bit mask as requires_ack().\n\
         Run: cargo test --test event_api event_header_flags_requires_ack"
    );
    assert!(
        !header.is_replay(),
        "PROPERTY: Flag bit 0x01 must NOT set is_replay().\n\
         Investigate: src/event/header.rs is_replay() flag mask.\n\
         Common causes: is_replay() using bit 0x01 instead of 0x08.\n\
         Run: cargo test --test event_api event_header_flags_requires_ack"
    );
}

#[test]
fn event_header_flags_transactional() {
    let header =
        EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA).with_flags(0x02);
    assert!(
        header.is_transactional(),
        "PROPERTY: Flag bit 0x02 must set is_transactional() to true.\n\
         Investigate: src/event/header.rs is_transactional() flag mask.\n\
         Common causes: is_transactional() testing bit 0x01 instead of 0x02, or \
         flag bits defined in the wrong order.\n\
         Run: cargo test --test event_api event_header_flags_transactional"
    );
    assert!(
        !header.requires_ack(),
        "PROPERTY: Flag bit 0x02 must NOT set requires_ack().\n\
         Investigate: src/event/header.rs requires_ack() flag mask.\n\
         Common causes: requires_ack() testing bit 0x02 instead of 0x01.\n\
         Run: cargo test --test event_api event_header_flags_transactional"
    );
}

#[test]
fn event_header_flags_replay() {
    let header =
        EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA).with_flags(0x08);
    assert!(
        header.is_replay(),
        "PROPERTY: Flag bit 0x08 must set is_replay() to true.\n\
         Investigate: src/event/header.rs is_replay() flag mask.\n\
         Common causes: is_replay() testing bit 0x04 instead of 0x08, or \
         flag constant defined incorrectly.\n\
         Run: cargo test --test event_api event_header_flags_replay"
    );
    assert!(
        !header.requires_ack(),
        "PROPERTY: Flag bit 0x08 must NOT set requires_ack().\n\
         Investigate: src/event/header.rs requires_ack() flag mask.\n\
         Common causes: requires_ack() mask accidentally overlapping with replay bit.\n\
         Run: cargo test --test event_api event_header_flags_replay"
    );
    assert!(
        !header.is_transactional(),
        "PROPERTY: Flag bit 0x08 must NOT set is_transactional().\n\
         Investigate: src/event/header.rs is_transactional() flag mask.\n\
         Common causes: is_transactional() mask accidentally overlapping with replay bit.\n\
         Run: cargo test --test event_api event_header_flags_replay"
    );
}

#[test]
fn event_header_flags_zero_all_false() {
    let header = EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA);
    assert!(
        !header.requires_ack(),
        "PROPERTY: Zero flags must not set requires_ack().\n\
         Investigate: src/event/header.rs requires_ack() flags field initialization.\n\
         Common causes: EventHeader::new() defaulting flags to a non-zero value, or \
         requires_ack() not masking correctly against 0x01.\n\
         Run: cargo test --test event_api event_header_flags_zero_all_false"
    );
    assert!(
        !header.is_transactional(),
        "PROPERTY: Zero flags must not set is_transactional().\n\
         Investigate: src/event/header.rs is_transactional() flags field initialization.\n\
         Common causes: EventHeader::new() defaulting flags to a non-zero value, or \
         is_transactional() not masking correctly against 0x02.\n\
         Run: cargo test --test event_api event_header_flags_zero_all_false"
    );
    assert!(
        !header.is_replay(),
        "PROPERTY: Zero flags must not set is_replay().\n\
         Investigate: src/event/header.rs is_replay() flags field initialization.\n\
         Common causes: EventHeader::new() defaulting flags to a non-zero value, or \
         is_replay() not masking correctly against 0x08.\n\
         Run: cargo test --test event_api event_header_flags_zero_all_false"
    );
}

#[test]
fn event_header_age_us() {
    let header = EventHeader::new(
        1,
        1,
        None,
        1_000_000,
        DagPosition::root(),
        0,
        EventKind::DATA,
    );
    let age = header.age_us(2_000_000);
    assert_eq!(
        age, 1_000_000,
        "PROPERTY: age_us(now) must return (now - timestamp_us) as the age in microseconds.\n\
         Investigate: src/event/header.rs EventHeader::age_us().\n\
         Common causes: age_us() returning absolute timestamp instead of delta, \
         or subtraction performed in wrong order (timestamp - now).\n\
         Run: cargo test --test event_api event_header_age_us"
    );
}

// ================================================================
// src/event/kind.rs — Classification + constants
// ================================================================

#[test]
fn event_kind_system_constants_are_system() {
    let system_kinds = [
        EventKind::DATA,
        EventKind::SYSTEM_INIT,
        EventKind::SYSTEM_SHUTDOWN,
        EventKind::SYSTEM_HEARTBEAT,
        EventKind::SYSTEM_CONFIG_CHANGE,
        EventKind::SYSTEM_CHECKPOINT,
    ];
    for kind in system_kinds {
        assert!(
            kind.is_system(),
            "PROPERTY: EventKind {:?} must return true for is_system().\n\
             Investigate: src/event/kind.rs EventKind::is_system() category check.\n\
             Common causes: system category constant changed, or is_system() checking \
             the wrong nibble in the packed u16 value.\n\
             Run: cargo test --test event_api event_kind_system_constants_are_system",
            kind
        );
        assert!(
            !kind.is_effect(),
            "PROPERTY: System EventKind {:?} must NOT return true for is_effect().\n\
             Investigate: src/event/kind.rs EventKind::is_effect() category check.\n\
             Common causes: is_effect() using the same category mask as is_system().\n\
             Run: cargo test --test event_api event_kind_system_constants_are_system",
            kind
        );
    }
}

#[test]
fn event_kind_effect_constants_are_effect() {
    let effect_kinds = [
        EventKind::EFFECT_ERROR,
        EventKind::EFFECT_RETRY,
        EventKind::EFFECT_ACK,
        EventKind::EFFECT_BACKPRESSURE,
        EventKind::EFFECT_CANCEL,
        EventKind::EFFECT_CONFLICT,
    ];
    for kind in effect_kinds {
        assert!(
            kind.is_effect(),
            "PROPERTY: EventKind {:?} must return true for is_effect().\n\
             Investigate: src/event/kind.rs EventKind::is_effect() category check.\n\
             Common causes: effect category constant changed, or is_effect() checking \
             the wrong nibble in the packed u16 value.\n\
             Run: cargo test --test event_api event_kind_effect_constants_are_effect",
            kind
        );
        assert!(
            !kind.is_system(),
            "PROPERTY: Effect EventKind {:?} must NOT return true for is_system().\n\
             Investigate: src/event/kind.rs EventKind::is_system() category check.\n\
             Common causes: is_system() using the same category mask as is_effect().\n\
             Run: cargo test --test event_api event_kind_effect_constants_are_effect",
            kind
        );
    }
}

#[test]
fn event_kind_custom_is_neither_system_nor_effect() {
    let custom = EventKind::custom(0x5, 42);
    assert!(
        !custom.is_system(),
        "PROPERTY: Custom EventKind must NOT be classified as a system event.\n\
         Investigate: src/event/kind.rs EventKind::is_system() category range check.\n\
         Common causes: is_system() treating category 0x5 as reserved system space, \
         or category boundaries defined incorrectly.\n\
         Run: cargo test --test event_api event_kind_custom_is_neither_system_nor_effect"
    );
    assert!(
        !custom.is_effect(),
        "PROPERTY: Custom EventKind must NOT be classified as an effect event.\n\
         Investigate: src/event/kind.rs EventKind::is_effect() category range check.\n\
         Common causes: is_effect() treating category 0x5 as reserved effect space, \
         or category boundaries defined incorrectly.\n\
         Run: cargo test --test event_api event_kind_custom_is_neither_system_nor_effect"
    );
    assert_eq!(
        custom.category(),
        0x5,
        "PROPERTY: EventKind::custom(0x5, 42) must store and return category 0x5.\n\
         Investigate: src/event/kind.rs EventKind::custom() category() packing.\n\
         Common causes: category packed into wrong nibble position, or category() \
         extracting the wrong bits from the u16.\n\
         Run: cargo test --test event_api event_kind_custom_is_neither_system_nor_effect"
    );
    assert_eq!(
        custom.type_id(),
        42,
        "PROPERTY: EventKind::custom(0x5, 42) must store and return type_id 42.\n\
         Investigate: src/event/kind.rs EventKind::custom() type_id() packing.\n\
         Common causes: type_id packed into wrong byte position, or type_id() \
         extracting bits that include the category nibble.\n\
         Run: cargo test --test event_api event_kind_custom_is_neither_system_nor_effect"
    );
}

#[test]
fn event_kind_display_hex() {
    let kind = EventKind::custom(0xA, 0xBC);
    let s = format!("{kind}");
    assert_eq!(
        s, "0xA0BC",
        "PROPERTY: EventKind Display must format as '0x{{category_nibble}}{{type_byte:02X}}' (4 hex digits).\n\
         Investigate: src/event/kind.rs EventKind Display impl.\n\
         Common causes: Display using lowercase instead of uppercase hex, missing '0x' prefix, \
         or printing the raw u16 without the structured nibble/byte layout.\n\
         Run: cargo test --test event_api event_kind_display_hex"
    );
}

// ================================================================
// src/event/mod.rs — Event convenience methods
// ================================================================

#[test]
fn event_with_hash_chain_sets_field() {
    let header = EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA);
    let chain = HashChain {
        prev_hash: [0u8; 32],
        event_hash: [1u8; 32],
    };
    let event = Event::new(header, "payload").with_hash_chain(chain.clone());

    assert_eq!(
        event.hash_chain,
        Some(chain),
        "PROPERTY: Event::with_hash_chain must store the provided HashChain in the hash_chain field.\n\
         Investigate: src/event/mod.rs Event::with_hash_chain().\n\
         Common causes: with_hash_chain() returning a clone of the original event without \
         updating the field, or hash_chain field shadowed by a local variable.\n\
         Run: cargo test --test event_api event_with_hash_chain_sets_field"
    );
}

#[test]
fn event_is_genesis_true_when_prev_hash_zero() {
    let header = EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA);
    let event = Event::new(header, ()).with_hash_chain(HashChain {
        prev_hash: [0u8; 32],
        event_hash: [1u8; 32],
    });
    assert!(
        event.is_genesis(),
        "PROPERTY: Event with all-zero prev_hash must be identified as a genesis event.\n\
         Investigate: src/event/mod.rs Event::is_genesis().\n\
         Common causes: is_genesis() checking event_hash instead of prev_hash, or \
         comparing against [0xFF; 32] instead of [0u8; 32].\n\
         Run: cargo test --test event_api event_is_genesis_true_when_prev_hash_zero"
    );
}

#[test]
fn event_is_genesis_false_when_prev_hash_nonzero() {
    let header = EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA);
    let event = Event::new(header, ()).with_hash_chain(HashChain {
        prev_hash: [0xFF; 32],
        event_hash: [1u8; 32],
    });
    assert!(
        !event.is_genesis(),
        "PROPERTY: Event with non-zero prev_hash must NOT be identified as a genesis event.\n\
         Investigate: src/event/mod.rs Event::is_genesis().\n\
         Common causes: is_genesis() always returning true, or using wrong zero-comparison \
         that ignores non-zero bytes in prev_hash.\n\
         Run: cargo test --test event_api event_is_genesis_false_when_prev_hash_nonzero"
    );
}

#[test]
fn event_is_genesis_true_when_no_hash_chain() {
    let header = EventHeader::new(1, 1, None, 0, DagPosition::root(), 0, EventKind::DATA);
    let event = Event::new(header, ());
    assert!(
        event.is_genesis(),
        "PROPERTY: Event with no hash_chain (None) must be treated as a genesis event.\n\
         Investigate: src/event/mod.rs Event::is_genesis().\n\
         Common causes: is_genesis() panicking or returning false on None hash_chain, \
         or not handling the Option::None case.\n\
         Run: cargo test --test event_api event_is_genesis_true_when_no_hash_chain"
    );
}

#[test]
fn event_map_payload_transforms_preserving_header() {
    let header = EventHeader::new(42, 42, None, 100, DagPosition::child(5), 0, EventKind::DATA);
    let event = Event::new(header, 21);
    let mapped = event.map_payload(|x| x * 2);
    assert_eq!(
        mapped.payload, 42,
        "PROPERTY: Event::map_payload must apply the closure to transform the payload.\n\
         Investigate: src/event/mod.rs Event::map_payload().\n\
         Common causes: map_payload() ignoring the closure and cloning the original \
         payload, or building a new Event with the old payload.\n\
         Run: cargo test --test event_api event_map_payload_transforms_preserving_header"
    );
    assert_eq!(
        mapped.header.event_id, 42,
        "PROPERTY: Event::map_payload must preserve the original header unchanged.\n\
         Investigate: src/event/mod.rs Event::map_payload().\n\
         Common causes: map_payload() creating a new header instead of moving the original, \
         or resetting header fields like event_id to defaults.\n\
         Run: cargo test --test event_api event_map_payload_transforms_preserving_header"
    );
    assert_eq!(
        mapped.header.timestamp_us, 100,
        "PROPERTY: Event::map_payload must preserve header.timestamp_us unchanged.\n\
         Investigate: src/event/mod.rs Event::map_payload().\n\
         Common causes: map_payload() rebuilding a fresh header that zero-initializes \
         timestamp_us instead of carrying over the original header.\n\
         Run: cargo test --test event_api event_map_payload_transforms_preserving_header"
    );
}

#[test]
fn event_position_returns_header_position() {
    let pos = DagPosition::child(7);
    let header = EventHeader::new(1, 1, None, 0, pos, 0, EventKind::DATA);
    let event = Event::new(header, ());
    assert_eq!(
        *event.position(),
        pos,
        "PROPERTY: Event::position() must return the DagPosition from the header.\n\
         Investigate: src/event/mod.rs Event::position().\n\
         Common causes: position() returning a default DagPosition instead of \
         delegating to header.position, or dereferencing the wrong field.\n\
         Run: cargo test --test event_api event_position_returns_header_position"
    );
}

// ================================================================
// src/coordinate/position.rs — DagPosition
// ================================================================

#[test]
fn dag_position_root() {
    let pos = DagPosition::root();
    assert_eq!(
        pos.depth, 0,
        "PROPERTY: DagPosition::root() must set depth to 0.\n\
         Investigate: src/coordinate/position.rs DagPosition::root().\n\
         Common causes: root() copying a non-zero depth from a template, or \
         the struct initializer using wrong field ordering.\n\
         Run: cargo test --test event_api dag_position_root"
    );
    assert_eq!(
        pos.lane, 0,
        "PROPERTY: DagPosition::root() must set lane to 0.\n\
         Investigate: src/coordinate/position.rs DagPosition::root().\n\
         Common causes: root() initializing lane to 1 instead of 0, or \
         field ordering swap between lane and depth.\n\
         Run: cargo test --test event_api dag_position_root"
    );
    assert_eq!(
        pos.sequence, 0,
        "PROPERTY: DagPosition::root() must set sequence to 0.\n\
         Investigate: src/coordinate/position.rs DagPosition::root().\n\
         Common causes: root() not zero-initializing sequence, or sequence \
         defaulting to 1 for 'first event' semantics.\n\
         Run: cargo test --test event_api dag_position_root"
    );
    assert!(
        pos.is_root(),
        "PROPERTY: DagPosition::root() must satisfy is_root().\n\
         Investigate: src/coordinate/position.rs DagPosition::is_root().\n\
         Common causes: is_root() checking only depth but not lane or sequence, \
         or root() not producing coordinates that satisfy is_root().\n\
         Run: cargo test --test event_api dag_position_root"
    );
}

#[test]
fn dag_position_child() {
    let pos = DagPosition::child(42);
    assert_eq!(
        pos.sequence, 42,
        "PROPERTY: DagPosition::child(42) must set sequence to 42.\n\
         Investigate: src/coordinate/position.rs DagPosition::child().\n\
         Common causes: child() ignoring the sequence argument and hardcoding 0, \
         or storing sequence in the depth field by mistake.\n\
         Run: cargo test --test event_api dag_position_child"
    );
    assert_eq!(
        pos.depth, 0,
        "PROPERTY: DagPosition::child() must set depth to 0 (same lane as root).\n\
         Investigate: src/coordinate/position.rs DagPosition::child().\n\
         Common causes: child() incrementing depth like fork(), or copying depth \
         from an earlier position.\n\
         Run: cargo test --test event_api dag_position_child"
    );
    assert_eq!(
        pos.lane, 0,
        "PROPERTY: DagPosition::child() must set lane to 0 (main lane).\n\
         Investigate: src/coordinate/position.rs DagPosition::child().\n\
         Common causes: child() calling fork() internally, which would assign a new lane.\n\
         Run: cargo test --test event_api dag_position_child"
    );
    assert!(
        !pos.is_root(),
        "PROPERTY: DagPosition::child(42) must NOT satisfy is_root() (non-zero sequence).\n\
         Investigate: src/coordinate/position.rs DagPosition::is_root().\n\
         Common causes: is_root() only checking depth==0 and lane==0 without checking \
         sequence==0.\n\
         Run: cargo test --test event_api dag_position_child"
    );
}

#[test]
fn dag_position_fork() {
    let pos = DagPosition::fork(2, 3);
    assert_eq!(
        pos.depth, 3,
        "PROPERTY: DagPosition::fork(parent_depth=2, lane=3) must set depth to parent_depth+1=3.\n\
         Investigate: src/coordinate/position.rs DagPosition::fork().\n\
         Common causes: fork() storing parent_depth unchanged instead of incrementing, \
         or depth and lane arguments swapped in the constructor.\n\
         Run: cargo test --test event_api dag_position_fork"
    );
    assert_eq!(
        pos.lane, 3,
        "PROPERTY: DagPosition::fork(2, 3) must set lane to 3.\n\
         Investigate: src/coordinate/position.rs DagPosition::fork().\n\
         Common causes: fork() using an auto-incrementing lane counter instead of \
         the provided lane argument.\n\
         Run: cargo test --test event_api dag_position_fork"
    );
    assert_eq!(
        pos.sequence, 0,
        "PROPERTY: DagPosition::fork() must start at sequence 0 (beginning of the new branch).\n\
         Investigate: src/coordinate/position.rs DagPosition::fork().\n\
         Common causes: fork() copying sequence from the parent position instead of \
         resetting it to 0.\n\
         Run: cargo test --test event_api dag_position_fork"
    );
    assert!(
        !pos.is_root(),
        "PROPERTY: DagPosition::fork() must NOT be is_root() (non-zero depth and lane).\n\
         Investigate: src/coordinate/position.rs DagPosition::is_root().\n\
         Common causes: is_root() returning true when sequence==0 regardless of depth/lane.\n\
         Run: cargo test --test event_api dag_position_fork"
    );
}

#[test]
fn dag_position_is_ancestor_of_same_lane() {
    let a = DagPosition::child(2);
    let b = DagPosition::child(5);
    assert!(
        a.is_ancestor_of(&b),
        "PROPERTY: child(2) must be an ancestor of child(5) on the same lane (lower sequence).\n\
         Investigate: src/coordinate/position.rs DagPosition::is_ancestor_of().\n\
         Common causes: is_ancestor_of() using >= instead of < on sequence, or not \
         checking lane equality before comparing sequence.\n\
         Run: cargo test --test event_api dag_position_is_ancestor_of_same_lane"
    );
    assert!(
        !b.is_ancestor_of(&a),
        "PROPERTY: child(5) must NOT be an ancestor of child(2) (higher sequence cannot be ancestor).\n\
         Investigate: src/coordinate/position.rs DagPosition::is_ancestor_of().\n\
         Common causes: is_ancestor_of() not checking direction, returning true for \
         any two positions on the same lane.\n\
         Run: cargo test --test event_api dag_position_is_ancestor_of_same_lane"
    );
}

#[test]
fn dag_position_is_ancestor_of_different_lanes() {
    let a = DagPosition::new(0, 0, 2);
    let b = DagPosition::new(0, 1, 5); // different lane
    assert!(
        !a.is_ancestor_of(&b),
        "PROPERTY: Positions on different lanes must never be ancestors of each other.\n\
         Investigate: src/coordinate/position.rs DagPosition::is_ancestor_of().\n\
         Common causes: is_ancestor_of() only comparing sequence without checking \
         that lanes match, treating all lower-sequence positions as ancestors.\n\
         Run: cargo test --test event_api dag_position_is_ancestor_of_different_lanes"
    );
}

#[test]
fn dag_position_partial_ord_same_lane() {
    let a = DagPosition::child(2);
    let b = DagPosition::child(5);
    assert!(
        a < b,
        "PROPERTY: child(2) must be less than child(5) on the same lane (lower sequence < higher).\n\
         Investigate: src/coordinate/position.rs DagPosition PartialOrd impl.\n\
         Common causes: PartialOrd returning None for same-lane comparisons, or comparing \
         depth/lane first without falling through to sequence.\n\
         Run: cargo test --test event_api dag_position_partial_ord_same_lane"
    );
    assert!(
        b > a,
        "PROPERTY: child(5) must be greater than child(2) on the same lane.\n\
         Investigate: src/coordinate/position.rs DagPosition PartialOrd impl.\n\
         Common causes: PartialOrd impl not providing symmetry, or gt() delegating \
         to an incorrect comparison.\n\
         Run: cargo test --test event_api dag_position_partial_ord_same_lane"
    );
    let c = DagPosition::child(2);
    assert!(
        a.partial_cmp(&c) == Some(std::cmp::Ordering::Equal),
        "PROPERTY: Two child(2) positions on the same lane must compare as Equal.\n\
         Investigate: src/coordinate/position.rs DagPosition PartialOrd impl.\n\
         Common causes: PartialOrd returning None for equal positions, or failing to \
         short-circuit when all fields are equal.\n\
         Run: cargo test --test event_api dag_position_partial_ord_same_lane"
    );
}

#[test]
fn dag_position_partial_ord_different_lanes_incomparable() {
    let a = DagPosition::new(0, 0, 2);
    let b = DagPosition::new(0, 1, 5);
    assert_eq!(
        a.partial_cmp(&b),
        None,
        "PROPERTY: DagPositions on different lanes must be incomparable (partial_cmp returns None).\n\
         Investigate: src/coordinate/position.rs DagPosition PartialOrd impl.\n\
         Common causes: PartialOrd ignoring lane differences and comparing only by sequence, \
         or returning Some(Less) when lane of a < lane of b.\n\
         Run: cargo test --test event_api dag_position_partial_ord_different_lanes_incomparable"
    );
}

#[test]
fn dag_position_display() {
    let pos = DagPosition::new(1, 2, 3);
    assert_eq!(
        format!("{pos}"),
        "1:2:3@0.0",
        "PROPERTY: DagPosition Display must format as 'depth:lane:sequence@wall_ms.counter'.\n\
         Investigate: src/coordinate/position.rs DagPosition Display impl.\n\
         Common causes: Display outputting fields in wrong order (e.g. sequence:lane:depth), \
         using a different separator than ':', or printing only some fields.\n\
         Run: cargo test --test event_api dag_position_display"
    );
}

// ================================================================
// DagPosition::fork() — scaffolding for multi-lane fan-out
// ================================================================

#[test]
fn dag_position_fork_creates_new_lane() {
    let forked = DagPosition::fork(0, 1);
    assert_eq!(
        forked,
        DagPosition::new(1, 1, 0),
        "PROPERTY: fork(parent_depth=0, new_lane=1) must produce depth=1, lane=1, sequence=0.\n\
         Investigate: src/coordinate/position.rs DagPosition::fork().\n\
         Run: cargo test --test event_api dag_position_fork_creates_new_lane"
    );
}

#[test]
fn dag_position_forked_incomparable_with_lane_zero() {
    let main = DagPosition::child(5);
    let forked = DagPosition::fork(0, 1);
    assert!(
        main.partial_cmp(&forked).is_none(),
        "PROPERTY: Forked position (lane=1) must be incomparable with main lane (lane=0).\n\
         Investigate: src/coordinate/position.rs DagPosition PartialOrd impl.\n\
         Run: cargo test --test event_api dag_position_forked_incomparable_with_lane_zero"
    );
}

#[test]
fn dag_position_fork_is_not_ancestor_across_lanes() {
    let main = DagPosition::child(0);
    let forked = DagPosition::fork(0, 1);
    assert!(
        !main.is_ancestor_of(&forked),
        "PROPERTY: is_ancestor_of must return false across different lanes.\n\
         Investigate: src/coordinate/position.rs DagPosition::is_ancestor_of().\n\
         Run: cargo test --test event_api dag_position_fork_is_not_ancestor_across_lanes"
    );
}