File size: 52,648 Bytes
5e0b58b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
"""Brain-5D command-line simulation entry point with dashboard integration.

This module runs the simulation and optionally starts the dashboard in the
main thread (to handle signals). The simulation runs in a daemon thread
so that the dashboard can control it via the OperatorBridge.

CLI semantics:

1. Dashboard mode (default):

   python -m src.main --config configs/poc_config.yaml

   -> starts RuntimeController in IDLE
   -> does NOT automatically advance ticks
   -> operator controls runtime through dashboard/API

2. Headless mode:

   python -m src.main --config configs/poc_structural_live.yaml --no-dashboard --ticks 500

   -> runs exactly N ticks through RuntimeController.run_ticks(N)

3. --ticks in dashboard mode only overrides the config value;
   the controller still starts IDLE.

Usage:
    python -m src.main --config configs/poc_config.yaml
    python -m src.main --config configs/poc_config.yaml --no-dashboard
    python -m src.main --config configs/poc_config.yaml --no-dashboard --ticks 500
    python -m src.main --config configs/poc_config.yaml --observe --benchmark
"""

from __future__ import annotations

import argparse
import random
import statistics
import sys
import threading
from dataclasses import asdict
from pathlib import Path
from typing import Any, Literal, cast

from src.config.loader import load_config

# ================================================================
# Canonical Runtime Controller
# ================================================================
from src.controller.runtime import PostTickHook
from src.controller.runtime import RuntimeController as _RuntimeController
from src.core import Brain5DConfig, NeuralNetwork
from src.core.spatial_index import (
    coords_to_linear,
    linear_to_5d,
    make_boundary_coord,
    unpack_coords,
)
from src.diagnostics.propagation import PropagationAnalyzer
from src.diagnostics.stimulus import StimulusEngine, StimulusResult
from src.diagnostics.topology_health import TopologyHealth
from src.experience import build_experience_subsystem
from src.homeostasis import HomeostasisEngine
from src.learning.learning_engine import LearningEngine

# ================================================================
# Snapshot Writer
# ================================================================
from src.storage import B5DSnapshotWriter
from src.telemetry.history import History
from src.telemetry.probes import ProbeManager
from src.telemetry.spike_history import SpikeHistory
from src.utils.run_artifacts import RunArtifacts
from src.version import BRAIN5D_VERSION_DISPLAY

# ================================================================
# Dashboard Integration – with None‑fallback
# ================================================================
_dashboard_available = False
_OperatorBridge: type | None = None
_serve_dashboard: Any = None
_DashboardStateStore: type | None = None

try:
    from src.dashboard.operator_bridge import OperatorBridge as _OperatorBridge
    from src.dashboard.server import serve_dashboard as _serve_dashboard
    from src.dashboard.state import DashboardStateStore as _DashboardStateStore

    _dashboard_available = True
except ImportError as e:
    print(f"⚠️ Dashboard not available: {e}")


# ================================================================
# Helper Functions
# ================================================================


def sample_positions_excluding_poc(
    total: int,
    reserved: set[int],
    n: int,
    rng: random.Random,
) -> list[int]:
    """Sample free linear positions while preserving reserved PoC cells."""
    available = [idx for idx in range(total) if idx not in reserved]
    if n > len(available):
        raise ValueError(
            f"Not enough unreserved positions: need {n}, have {len(available)}"
        )
    return rng.sample(available, n)


def build_network(config_dict: dict[str, Any]) -> tuple[NeuralNetwork, random.Random]:
    """Build the network from configuration using the new Brain5DConfig."""
    # Convert dict to Brain5DConfig
    config = Brain5DConfig.from_dict(config_dict)
    rng = random.Random(int(config_dict.get("seed", 42)))

    # Create network
    network = NeuralNetwork(config, rng)

    dims = config.dimensions

    # Extract topology information from the original config dict
    topology = config_dict.get("topology", {})
    input_dim = topology.get("input", {}).get("dimension", "x")
    input_coord = topology.get("input", {}).get("coordinate", 0)
    output_dim = topology.get("output", {}).get("dimension", "x")
    output_coord = topology.get("output", {}).get("coordinate", dims[0] - 1)

    # Add reserved neurons (input, output, diagnostic)
    input_coord_5d = make_boundary_coord(dims, input_dim, input_coord)
    output_coord_5d = make_boundary_coord(dims, output_dim, output_coord)
    diag_coord = tuple(
        config_dict.get("diagnostics", {}).get("target_coord", (0, 0, 0, 0, 0))
    )

    reserved_coords = {input_coord_5d, output_coord_5d, diag_coord}
    reserved_indices = {coords_to_linear(coord, dims) for coord in reserved_coords}

    total_positions = 1
    for d in dims:
        total_positions *= d

    initial_neurons = int(config_dict.get("initial_neurons", 5000))

    chosen = sample_positions_excluding_poc(
        total_positions,
        reserved_indices,
        initial_neurons - len(reserved_coords),
        rng,
    )

    for idx in chosen:
        network.add_neuron(linear_to_5d(idx, dims))

    for coord in sorted(reserved_coords):
        network.add_neuron(coord)

    # Set input/output cells
    network.set_input_output_cells(
        input_dim,
        input_coord,
        output_dim,
        output_coord,
    )

    # Initialize random connections
    conn_per_neuron = config.network.initial_connections_per_neuron
    radius = config.network.neighbour_radius
    network.initialize_random_connections(conn_per_neuron, radius)

    return network, rng


def setup_learning(
    network: NeuralNetwork,
    config_dict: dict[str, Any],
) -> LearningEngine | None:
    """Set up and attach the learning engine if enabled."""
    try:
        learning = LearningEngine(network, config_dict)
        if learning.enabled:
            learning.attach()
            print("✅ Learning engine attached")
        return learning
    except Exception as e:
        print(f"⚠️ Learning engine setup failed: {e}")
        return None


def setup_homeostasis(
    network: NeuralNetwork,
    config_dict: dict[str, Any],
) -> HomeostasisEngine | None:
    """Set up and attach the homeostasis engine if enabled."""
    try:
        homeostasis = HomeostasisEngine(network, config_dict)
        if homeostasis.enabled:
            homeostasis.attach()
            print("✅ Homeostasis engine attached")
        return homeostasis
    except Exception as e:
        print(f"⚠️ Homeostasis engine setup failed: {e}")
        return None


def setup_observatory(
    network: NeuralNetwork,
    config_dict: dict[str, Any],
    spike_history: SpikeHistory,
    history: History,
    probes: ProbeManager,
) -> Any | None:
    """Set up the observatory if visualization is enabled."""
    vis = config_dict.get("visualization", {})
    if not vis.get("enabled", False):
        return None

    try:
        from src.visualization.observatory import Observatory

        observatory = Observatory(network, config_dict, spike_history, history, probes)
        print("✅ Observatory ready")
        return observatory
    except Exception as e:
        print(f"⚠️ Observatory setup failed: {e}")
        return None


# ================================================================
# Main
# ================================================================


def _configure_utf8_streams() -> None:
    """Ensure stdout/stderr can emit Unicode even on cp1252 consoles."""
    for stream_name in ("stdout", "stderr"):
        stream = getattr(sys, stream_name)
        if stream is not None and hasattr(stream, "reconfigure"):
            try:
                stream.reconfigure(encoding="utf-8", errors="replace")
            except Exception:
                pass


def main() -> int:
    _configure_utf8_streams()
    parser = argparse.ArgumentParser(
        description="Brain 5D v0.5 - homeostatic self-regulation with dashboard"
    )
    parser.add_argument("--config", default="configs/poc_config.yaml")
    parser.add_argument("--observe", action="store_true")
    parser.add_argument("--benchmark", action="store_true")
    parser.add_argument("--no-dashboard", action="store_true")
    parser.add_argument("--no-learning", action="store_true")
    parser.add_argument("--no-homeostasis", action="store_true")
    parser.add_argument(
        "--dashboard-host",
        default="127.0.0.1",
        help="Dashboard bind host (default: 127.0.0.1; use 0.0.0.0 for LAN)",
    )
    parser.add_argument(
        "--dashboard-port",
        type=int,
        default=8765,
        help="Dashboard HTTP server port (default: 8765)",
    )
    parser.add_argument(
        "--ticks",
        type=int,
        default=None,
        help="Override config ticks. In dashboard mode (default) this only updates config;\n                         the controller starts IDLE and must be advanced via API.\n                         Use --no-dashboard for automatic execution.",
    )
    args = parser.parse_args()

    # Load configuration
    config_dict: dict[str, Any] = cast(dict[str, Any], load_config(args.config))
    if args.observe:
        config_dict["visualization"] = config_dict.get("visualization", {})
        config_dict["visualization"]["enabled"] = True
    if args.ticks is not None:
        config_dict["ticks"] = args.ticks

    # Compute config SHA-256 for provenance
    import hashlib

    try:
        _effective_config_path = Path(args.config).resolve()
        _config_bytes = _effective_config_path.read_bytes()
        config_dict["_sha256"] = hashlib.sha256(_config_bytes).hexdigest()
        config_dict["_path"] = str(_effective_config_path)
    except Exception:
        config_dict["_sha256"] = ""
        config_dict["_path"] = str(args.config)

    print(f"🚀 Brain 5D - v{BRAIN5D_VERSION_DISPLAY} with dashboard")
    print(f"📄 Config: {args.config} (sha256={config_dict['_sha256'][:16]}...)")

    # --- Build network ---
    network, rng = build_network(config_dict)

    # --- Setup engines ---
    learning = None if args.no_learning else setup_learning(network, config_dict)
    homeostasis = (
        None if args.no_homeostasis else setup_homeostasis(network, config_dict)
    )

    # --- Topology health ---
    health = cast(dict[str, Any], TopologyHealth(network).analyze())  # type: ignore[reportUnknownMemberType]

    # --- Telemetry ---
    telemetry_cfg = config_dict.get("telemetry", {})
    history = History(int(telemetry_cfg.get("history_ticks", 10000)))
    spike_history = SpikeHistory(int(telemetry_cfg.get("spike_history_ticks", 1000)))
    probes = ProbeManager(network, config_dict)

    # --- Stimulus ---
    stimulus = StimulusEngine(config_dict, rng)

    # --- Propagation ---
    propagation = PropagationAnalyzer(network.output_cells)

    # --- Diagnostic probe ---
    diag_target = tuple(
        config_dict.get("diagnostics", {}).get("target_coord", (0, 0, 0, 0, 0))
    )
    diag_id = next(
        (nid for nid in network.neurons if unpack_coords(nid) == diag_target),
        None,
    )
    if diag_id is not None:
        probes.add_probe(diag_id)

    # --- Observatory ---
    observatory = setup_observatory(
        network, config_dict, spike_history, history, probes
    )

    # ================================================================
    # Canonical RuntimeController Setup
    # ================================================================
    dashboard_stop_event = threading.Event()
    operator_bridge = None
    state_store = None
    controller = None

    # Shared telemetry holders (filled by hooks, read by dashboard publisher)
    _storage_telemetry: dict[str, Any] = {"available": False}
    _self_org_stats: dict[str, Any] = {"available": False}

    def _self_org_stats_func() -> dict[str, Any]:
        return _self_org_stats

    # Telemetry and logging state (shared via closure)
    core_times: list[float] = []
    warmup = 100

    # Snapshot path
    _snapshot_dir = Path("artifacts")
    _snapshot_dir.mkdir(parents=True, exist_ok=True)
    _snapshot_path = _snapshot_dir / "latest.b5d"
    _snapshot_temp = _snapshot_dir / "latest.b5d.tmp"

    # Snapshot writer
    _snapshot_writer = B5DSnapshotWriter(restart_capable=True)

    def _write_snapshot() -> None:
        """Write a .b5d snapshot atomically (temp file → validate → rename)."""
        try:
            # Collect metadata
            git_commit = "unknown"
            git_dirty = True
            try:
                import subprocess

                result = subprocess.run(
                    ["git", "rev-parse", "HEAD"],
                    capture_output=True,
                    text=True,
                    timeout=5,
                    cwd=Path(__file__).resolve().parents[1],
                )
                if result.returncode == 0:
                    git_commit = result.stdout.strip()
                dirty_result = subprocess.run(
                    ["git", "status", "--porcelain"],
                    capture_output=True,
                    text=True,
                    timeout=5,
                    cwd=Path(__file__).resolve().parents[1],
                )
                git_dirty = bool(dirty_result.stdout.strip())
            except Exception:
                pass

            from src.storage.b5d import JSONMapping

            metadata: JSONMapping = {
                "type": "brain5d-snapshot",
                "version": BRAIN5D_VERSION_DISPLAY,
                "tick": network.current_tick,
                "neuron_count": len(network.neurons),
                "synapse_count": network.synapse_count,
                "dimensions": list(network.dimensions),
                "seed": config_dict.get("seed", 42),
                "git_commit": git_commit,
                "git_dirty": git_dirty,
                "config": {
                    "path": str(args.config),
                    "sha256": config_dict.get("_sha256", ""),
                },
            }

            # Write to temp file
            from src.storage.b5d import NetworkSnapshotLike

            _snapshot_writer.write(
                str(_snapshot_temp),
                cast("NetworkSnapshotLike", network),
                metadata=metadata,
            )

            # Validate written file (reader must be closed before rename on Windows)
            from src.storage.b5d import B5DReader

            reader = B5DReader(str(_snapshot_temp))
            try:
                reader.validate_invariants(full_scan=False)
            finally:
                reader.close()

            # Atomic rename
            _snapshot_temp.replace(_snapshot_path)

            # Preserve immutable historical snapshot with timestamp
            import time as _time

            tick = network.current_tick
            ts = _time.strftime("%Y%m%d_%H%M%S")
            historical = _snapshot_dir / f"snapshot_t{tick}_{ts}.b5d"
            try:
                import shutil

                shutil.copy2(_snapshot_path, historical)
            except Exception:
                pass

        except Exception as exc:
            print(f"⚠️ Snapshot write failed: {type(exc).__name__}: {exc}")
            # Clean up temp file on failure
            try:
                _snapshot_temp.unlink(missing_ok=True)
            except Exception:
                pass

    # ================================================================
    # Dashboard State Publishing Helper
    # ================================================================

    def _publish_dashboard_state(
        state_store: Any,
        network: NeuralNetwork,
        result: Any,
        learning: Any,
        homeostasis: Any,
        storage_telemetry: dict[str, Any],
        self_org_stats: dict[str, Any],
        status: str,
    ) -> None:
        """Build and publish a DashboardSnapshot from live runtime data."""
        from src.dashboard.health_builder import enrich_snapshot
        from src.dashboard.models import (
            DashboardSnapshot,
            HomeostasisMetrics,
            LearningMetrics,
            NetworkMetrics,
            SelfOrganizationMetrics,
            SpikeMetrics,
            StorageMetrics,
            SystemMetrics,
        )

        learning_stats = learning.stats if learning is not None else None
        homeo_stats = homeostasis.stats if homeostasis is not None else None

        # Storage telemetry: only populate if available
        storage_available = storage_telemetry.get("available", False)
        storage = StorageMetrics(available=storage_available)
        if storage_available:
            storage = StorageMetrics(
                available=True,
                queue_depth=storage_telemetry.get("queue_depth"),
                queue_capacity=storage_telemetry.get("queue_capacity"),
                batches_enqueued=storage_telemetry.get("batches_enqueued"),
                batches_written=storage_telemetry.get("batches_written"),
                deltas_written=storage_telemetry.get("deltas_written"),
                bytes_written=storage_telemetry.get("bytes_written"),
                dropped_batches=storage_telemetry.get("dropped_batches"),
                write_latency_ms=storage_telemetry.get("write_latency_ms"),
                commit_latency_ms=storage_telemetry.get("commit_latency_ms"),
                journal_size_bytes=storage_telemetry.get("journal_size_bytes"),
                worker_failed=storage_telemetry.get("worker_failed"),
            )

        # Self-organization metrics: only populate if available
        so_available = self_org_stats.get("available", False)
        self_org = SelfOrganizationMetrics(available=so_available)
        if so_available:
            self_org = SelfOrganizationMetrics(
                available=True,
                neurons_created=self_org_stats.get("neurons_created"),
                neurons_removed=self_org_stats.get("neurons_removed"),
                synapses_created=self_org_stats.get("synapses_created"),
                synapses_pruned=self_org_stats.get("synapses_pruned"),
            )

        snapshot = DashboardSnapshot(
            status=status,
            version=BRAIN5D_VERSION_DISPLAY,
            system=SystemMetrics(
                tick=result.tick,
                neurons=len(network.neurons),
                synapses=getattr(result, "total_synapses", network.synapse_count),
                spikes_total=result.total_spikes,
                spikes_last_tick=getattr(result, "spikes_this_tick", 0),
                core_step_ms=getattr(result, "core_step_ms", 0.0),
                mean_energy=getattr(result, "mean_energy", 0.0),
            ),
            learning=LearningMetrics(
                stdp_updates=getattr(learning_stats, "stdp_weight_updates", 0),
                reward_updates=getattr(learning_stats, "reward_weight_updates", 0),
                rewards_received=getattr(learning_stats, "rewards_received", 0),
                rewards_applied=getattr(learning_stats, "rewards_applied", 0),
                pending_rewards=getattr(learning_stats, "pending_rewards", 0),
                update_ms=getattr(learning_stats, "last_update_ms", 0.0),
                engine_attached=learning is not None,
                stdp_enabled=bool(config_dict.get("stdp", {}).get("enabled", False)),
                eligibility_enabled=bool(
                    config_dict.get("eligibility", {}).get("enabled", False)
                ),
                reward_enabled=bool(
                    config_dict.get("reward", {}).get("enabled", False)
                ),
            ),
            storage=storage,
            self_organization=self_org,
            homeostasis=HomeostasisMetrics(
                enabled=getattr(homeo_stats, "enabled", False),
                target_rate_hz=getattr(homeo_stats, "target_rate_hz", 0.0),
                actual_rate_hz=getattr(homeo_stats, "mean_rate_hz", 0.0),
                rate_error_hz=getattr(homeo_stats, "mean_rate_error_hz", 0.0),
                mean_rate_hz=getattr(homeo_stats, "mean_rate_hz", 0.0),
                mean_rate_error_hz=getattr(homeo_stats, "mean_rate_error_hz", 0.0),
                mean_threshold_adaptation=getattr(
                    homeo_stats, "mean_threshold_adaptation", 0.0
                ),
                target_energy=getattr(homeo_stats, "target_energy", 0.0),
                mean_energy=getattr(homeo_stats, "mean_energy", 0.0),
                mean_energy_error=getattr(homeo_stats, "mean_energy_error", 0.0),
                active_neurons=getattr(homeo_stats, "active_neurons", 0),
                updates=getattr(homeo_stats, "updates", 0),
            ),
            spikes=SpikeMetrics(
                total_spikes=result.total_spikes,
                active_neurons=len(getattr(result, "spike_ids", ())),
                mean_firing_rate_hz=0.0,
                burst_index=0.0,
                synchrony=0.0,
                spike_count_last_tick=getattr(result, "spikes_this_tick", 0),
            ),
            network=NetworkMetrics(
                tick=result.tick,
                neuron_count=len(network.neurons),
                synapse_count=getattr(result, "total_synapses", network.synapse_count),
                active_neurons=len(getattr(result, "spike_ids", ())),
                silent_neurons=len(network.neurons)
                - len(getattr(result, "spike_ids", ())),
                mean_firing_rate_hz=0.0,
                burst_index=0.0,
                synchrony=0.0,
                mean_energy=getattr(result, "mean_energy", 0.0),
                mean_threshold_adaptation=0.0,
                e_i_ratio=0.0,
                clustering_coefficient=0.0,
                mean_path_length=0.0,
            ),
            runtime={
                "config_path": str(config_dict.get("_path", "")),
                "config_sha256": str(config_dict.get("_sha256", "")),
                "state_publish_interval_ticks": int(
                    config_dict.get("dashboard", {}).get(
                        "state_publish_interval_ticks", 10
                    )
                ),
            },
        )
        state_store.publish(enrich_snapshot(snapshot, config_dict))

    try:
        controller = _RuntimeController(
            network=network,
            homeostasis=None,  # Homeostasis is attached as post-step hook
            batch_size=10,
            loop_delay_ms=0.0,
            telemetry_interval_ticks=10,
            snapshot_callback=_write_snapshot,
        )
        print("✅ Canonical RuntimeController created (idle)")

        experience = build_experience_subsystem(config_dict, network, learning)
        if experience is not None:
            experience.attach_runtime(controller)
            print("✅ ExperienceEngine attached via runtime hooks")

        # Shared state for stimulus result (set by pre-hook, read by post-hook)
        _last_stim: list[StimulusResult | None] = [None]

        # Register pre-tick hook for stimulus
        def _pre_tick(tick: int) -> None:
            if dashboard_stop_event.is_set():
                return
            _last_stim[0] = stimulus.apply(network, tick)  # type: ignore[reportUnknownMemberType]

        controller.add_pre_hook(_pre_tick)

        # Register post-tick hook for telemetry, artifacts, logging, dashboard
        def _on_tick(tick: int, result: Any) -> None:
            if dashboard_stop_event.is_set():
                return
            stim_result = _last_stim[0]

            # Reward
            reward_cfg = config_dict.get("reward", {})
            if (
                learning
                and learning.params.reward_enabled
                and reward_cfg.get("reward_source", "external") == "output_spike"
                and result.output_spike_ids
            ):
                learning.set_reward(
                    float(reward_cfg.get("output_spike_value", 1.0)),
                    result.tick,
                )

            # Telemetry
            history.append_from_stepresult(result)  # type: ignore[reportUnknownMemberType]
            spike_history.append(result.tick, result.spike_ids)
            if stim_result is not None:
                propagation.observe(stim_result, result)

            # Artifacts
            metric = history.get_all()[-1]  # type: ignore[reportUnknownVariableType]
            artifacts.log_metrics(metric)  # type: ignore[reportUnknownArgumentType]
            artifacts.log_spikes(result.tick, result.spike_ids)
            if stim_result is not None:
                artifacts.log_stimulus(stim_result)

            # Benchmark
            if args.benchmark and result.tick >= warmup:
                core_times.append(result.core_step_ms)

            # Console logging
            log_cfg = config_dict.get("logging", {})
            log_interval = log_cfg.get("interval_ticks", 100)
            if (result.tick + 1) % log_interval == 0:
                print(
                    f"Tick {result.tick + 1:4d} | spikes={result.spikes_this_tick:4d} "
                    f"| total={result.total_spikes:6d} "
                    f"| queue={result.queued_events:5d} "
                    f"| {result.core_step_ms:.3f} ms"
                )

            # Snapshot enrichment traverses all dashboard components/parameters;
            # publish at a bounded cadence instead of taxing every simulation tick.
            _dashboard_cfg = config_dict.get("dashboard", {})
            _publish_interval = max(
                1, int(_dashboard_cfg.get("state_publish_interval_ticks", 10))
            )
            if state_store is not None and (result.tick + 1) % _publish_interval == 0:
                try:
                    _publish_dashboard_state(
                        state_store=state_store,
                        network=network,
                        result=result,
                        learning=learning,
                        homeostasis=homeostasis,
                        storage_telemetry=_storage_telemetry,
                        self_org_stats=_self_org_stats_func(),
                        status="running",
                    )
                except Exception:
                    pass  # Dashboard publishing must never break simulation

        controller.add_hook(_on_tick)

        # --- Observatory ---
        vis_cfg = config_dict.get("visualization", {})
        refresh_interval = vis_cfg.get("refresh_interval_ticks", 100)
        if observatory:
            controller.add_hook(
                lambda t, r=None: (
                    observatory.draw() if (t + 1) % refresh_interval == 0 else None
                )
            )

    except Exception as e:
        print(f"⚠️ RuntimeController setup failed: {e}")
        controller = None

    # ================================================================
    # Self-Organization Setup (optional, verdrahtet Coordinator + Plasticity + Journal)
    # ================================================================
    _self_org_coordinator = None
    _self_org_plasticity = None
    _self_org_approval_policy = None
    _structural_journal = None

    so_cfg = config_dict.get("self_organization", {})
    if so_cfg.get("enabled", False) and controller is not None:
        try:
            from src.self_organization.composition import compose_structural_subsystem

            # Config-authoritative plasticity limits: derive from YAML
            _allow_neurogenesis = bool(so_cfg.get("neurogenesis_enabled", True))
            _allow_neuron_pruning = bool(so_cfg.get("pruning_enabled", False))
            _allow_synapse_sprouting = bool(so_cfg.get("sprouting_enabled", False))
            _allow_synapse_pruning = bool(so_cfg.get("synapse_pruning_enabled", False))
            _max_changes = int(so_cfg.get("neurogenesis_max_per_cycle", 1))

            _structural_journal_path = _snapshot_dir / "structural.journal"
            _composed = compose_structural_subsystem(
                network,
                _structural_journal_path,
                coordinator_enabled=True,
                coordinator_dry_run=False,
                max_changes_per_tick=_max_changes,
                allow_neurogenesis=_allow_neurogenesis,
                allow_neuron_pruning=_allow_neuron_pruning,
                allow_synapse_sprouting=_allow_synapse_sprouting,
                allow_synapse_pruning=_allow_synapse_pruning,
            )
            _self_org_plasticity = _composed["plasticity"]
            _self_org_coordinator = _composed["coordinator"]
            _structural_journal = _composed["journal"]
            _manipulator = _composed["manipulator"]

            # Build the canonical approval policy from config so the dashboard
            # can read the real self-organization gate state.
            from src.self_organization.approval import (
                ProposalApprovalPolicy,
                StructuralPlasticityConfig,
            )

            _self_org_approval_policy = ProposalApprovalPolicy(
                StructuralPlasticityConfig(
                    enabled=True,
                    dry_run=False,
                    auto_approval=bool(so_cfg.get("auto_approval", False)),
                    auto_approval_threshold=float(
                        so_cfg.get("auto_approval_threshold", 0.8)
                    ),
                    max_changes_per_tick=int(so_cfg.get("max_changes_per_tick", 5)),
                    max_neuron_additions_per_tick=int(
                        so_cfg.get("neurogenesis_max_per_cycle", 1)
                    ),
                    max_neuron_removals_per_tick=int(
                        so_cfg.get("max_neuron_removals_per_tick", 0)
                    ),
                    max_synapse_additions_per_tick=int(
                        so_cfg.get("sprouting_max_out_degree", 5)
                    ),
                    max_synapse_removals_per_tick=int(
                        so_cfg.get("max_synapse_removals_per_tick", 5)
                    ),
                    min_neurons=int(so_cfg.get("min_neurons", 100)),
                    max_neurons=int(so_cfg.get("max_neurons", 100_000)),
                    allow_neuron_pruning=_allow_neuron_pruning,
                    allow_synapse_pruning=_allow_synapse_pruning,
                    cooldown_ticks=int(so_cfg.get("cooldown_ticks", 100)),
                )
            )

            # The legacy SelfOrganizationEngine ran_cycle() mutates the
            # network directly through its own manipulator, bypassing the
            # canonical Coordinator -> Approval -> PlasticityEngine path.
            # For Alpha.5, structural mutation must flow exclusively through
            # the canonical path. The legacy engine is NOT attached.
            # It remains available for proposal-generation research only.

            # Update self-org telemetry
            _self_org_stats.update(
                available=True,
                neurons_created=0,
                neurons_removed=0,
                synapses_created=0,
                synapses_pruned=0,
            )

            print("✅ SelfOrganizationCoordinator + PlasticityEngine + Journal created")
            print(
                "   (canonical path only; legacy SelfOrganizationEngine NOT attached)"
            )

            # Attach the SelfOrganizationRuntimeAdapter as a post-tick hook.
            # This feeds real HomeostasisSignals through the policy and into
            # the coordinator — closing the production signal->policy->coordinator
            # path. The adapter does NOT mutate the network.
            #
            # CONFIG-AUTHORITATIVE: interval_ticks and policy_config are
            # derived from the YAML self_organization section. Hardcoded
            # defaults are never used when production config exists.
            if homeostasis is not None:
                try:
                    from src.self_organization.policy import (
                        SelfOrganizationPolicyConfig,
                    )
                    from src.self_organization.runtime_adapter import (
                        SelfOrganizationRuntimeAdapter,
                    )

                    _so_interval = int(so_cfg.get("interval_ticks", 100))
                    _so_policy_config = SelfOrganizationPolicyConfig.from_config(
                        config_dict
                    )
                    _so_adapter = SelfOrganizationRuntimeAdapter(
                        homeostasis_engine=homeostasis,
                        coordinator=_self_org_coordinator,
                        interval_ticks=_so_interval,
                        policy_config=_so_policy_config,
                    )
                    controller.add_hook(_so_adapter)
                    print(
                        f"   ✅ SelfOrganizationRuntimeAdapter attached (interval={_so_interval}, config-authoritative)"
                    )
                except Exception as adapter_err:
                    print(f"   ⚠️ SelfOrganizationRuntimeAdapter failed: {adapter_err}")
        except Exception as e:
            print(f"⚠️ Self-organization setup failed: {e}")

    # ================================================================
    # Runtime Delta Persistence (AsyncStorageSession)
    # ------------------------------------------------
    # CONFIG-AUTHORITATIVE: This subsystem is ONLY started when the
    # authoritative config enables it. poc_config.yaml has
    #   storage.enabled = false
    #   storage.runtime.enabled = false
    # so the dashboard must show "disabled by config" for Delta Storage,
    # NOT fake zeros from a silently-active worker.
    #
    # The four persistence systems are deliberately separated:
    #   1. Snapshot Service           -> always on (dashboard inspection)
    #   2. Runtime Delta Persistence   -> gated by storage.runtime.enabled
    #   3. Structural Journal          -> gated by self_organization.enabled
    #   4. Runtime Checkpoint          -> gated by storage.checkpoint.enabled
    # ================================================================
    _storage_cfg_raw = config_dict.get("storage", {})
    _storage_cfg: dict[str, Any] = (
        cast("dict[str, Any]", _storage_cfg_raw)
        if isinstance(_storage_cfg_raw, dict)
        else {}
    )
    _storage_runtime_cfg_raw = _storage_cfg.get("runtime", {})
    _storage_runtime_cfg: dict[str, Any] = (
        cast("dict[str, Any]", _storage_runtime_cfg_raw)
        if isinstance(_storage_runtime_cfg_raw, dict)
        else {}
    )
    _journal_cfg_raw = _storage_cfg.get("journal", {})
    _journal_cfg: dict[str, Any] = (
        cast("dict[str, Any]", _journal_cfg_raw)
        if isinstance(_journal_cfg_raw, dict)
        else {}
    )
    _storage_runtime_enabled = bool(_storage_cfg.get("enabled", False)) and bool(
        _storage_runtime_cfg.get("enabled", False)
    )

    if _storage_runtime_enabled:
        try:
            from src.storage.async_runtime import (
                AsyncStorageConfig,
                AsyncStorageSession,
            )
            from src.storage.delta_journal import DeltaJournal, JournalCorruptionError
            from src.storage.runtime import StorageRuntimeConfig

            _journal_path = _snapshot_dir / "latest.b5d.journal"
            _storage_runtime_config = StorageRuntimeConfig(
                snapshot_path=_snapshot_path,
                journal_path=_journal_path,
                commit_interval_ticks=int(
                    _journal_cfg.get("commit_interval_ticks", 10)
                ),
                capture_policy=cast(
                    "Literal['full_change_scan', 'dirty_tracking']",
                    _storage_runtime_cfg.get("capture_policy", "full_change_scan"),
                ),
            )
            _delta_journal = DeltaJournal(str(_journal_path))
            try:
                _delta_journal.open()
            except JournalCorruptionError as _journal_err:
                # A corrupt journal from a previous run would prevent startup.
                # Alpha.5 uses the journal as runtime delta persistence; losing
                # the tail is acceptable because the canonical .b5d snapshot is
                # the source of truth.  Rename the corrupt file and start fresh.
                _corrupt_path = _journal_path.with_suffix(".b5d.journal.corrupt")
                try:
                    _corrupt_path.unlink(missing_ok=True)
                    _journal_path.rename(_corrupt_path)
                except Exception:
                    pass
                _delta_journal = DeltaJournal(str(_journal_path))
                _delta_journal.open()
                print(
                    f"⚠️  Reset corrupt delta journal ({_journal_err}); "
                    f"old file preserved at {_corrupt_path.name}"
                )
            if _delta_journal.last_tick > network.current_tick:
                import time as _time

                previous_last_tick = _delta_journal.last_tick
                _delta_journal.close()
                _restart_archive = _journal_path.with_name(
                    f"{_journal_path.name}.restart-{previous_last_tick}-"
                    f"{_time.time_ns()}"
                )
                _journal_path.replace(_restart_archive)
                _delta_journal = DeltaJournal(str(_journal_path))
                _delta_journal.open()
                print(
                    "ℹ️ Archived delta journal from a later runtime "
                    f"(last_tick={previous_last_tick}, "
                    f"current_tick={network.current_tick}) as "
                    f"{_restart_archive.name}"
                )
            _delta_journal.close()
            from src.storage.runtime import RuntimeNetworkLike

            _async_config = AsyncStorageConfig()
            _async_storage = AsyncStorageSession(
                network=cast("RuntimeNetworkLike", network),
                runtime_config=_storage_runtime_config,
                async_config=_async_config,
            )
            _async_storage.start()

            # Periodisch Telemetrie auslesen
            def _update_storage_telemetry() -> None:
                try:
                    tel = _async_storage.telemetry
                    _storage_telemetry.update(
                        available=True,
                        queue_depth=tel.queue_depth,
                        queue_capacity=tel.queue_capacity,
                        batches_enqueued=tel.batches_enqueued,
                        batches_written=tel.batches_written,
                        deltas_written=tel.deltas_written,
                        bytes_written=tel.bytes_written,
                        dropped_batches=tel.dropped_batches,
                        write_latency_ms=tel.write_latency_ms,
                        commit_latency_ms=tel.commit_latency_ms,
                        journal_size_bytes=(
                            _delta_journal.path.stat().st_size
                            if _delta_journal.path.exists()
                            else 0
                        ),
                        worker_failed=tel.worker_failed,
                    )
                except Exception:
                    pass

            # Storage-Telemetrie im post-tick hook aktualisieren
            if controller is not None:
                controller.add_hook(lambda _tick, _result: _update_storage_telemetry())

            print("✅ AsyncStorageSession attached with telemetry (config-enabled)")
        except Exception as e:
            print(f"⚠️ Storage telemetry setup failed: {type(e).__name__}: {e}")
    else:
        # Storage is disabled by config — keep telemetry explicitly unavailable
        # so the dashboard renders "disabled by config" instead of fake zeros.
        _storage_telemetry.update(available=False)
        print(
            "ℹ️ Runtime Delta Persistence: disabled by config (storage.runtime.enabled=false)"
        )

    # ================================================================
    # OperatorBridge & Dashboard Setup
    # ================================================================
    if not args.no_dashboard and _dashboard_available and controller is not None:
        try:
            # Create TelemetryFrameStore for atomic live visualization
            # Read telemetry config from config_dict, fall back to defaults
            _dashboard_cfg: dict[str, Any] = config_dict.get("dashboard", {})  # type: ignore[type-arg]
            _live_telemetry_cfg: dict[str, Any] = _dashboard_cfg.get("live_telemetry", {})  # type: ignore[type-arg]
            _lt_enabled = bool(_live_telemetry_cfg.get("enabled", True))
            _lt_capture = int(_live_telemetry_cfg.get("capture_interval_ticks", 5))
            _lt_window = int(_live_telemetry_cfg.get("activity_window_ticks", 20))
            _sim_cfg: dict[str, Any] = config_dict.get("simulation", {})  # type: ignore[type-arg]
            _sim_dt_ms = float(_sim_cfg.get("dt_ms", 1.0))

            _telemetry_store: Any = None
            if _lt_enabled:
                from src.dashboard.live_projection import (
                    TelemetryFrameStore,
                    make_telemetry_hook,
                )

                _telemetry_store = TelemetryFrameStore(
                    capture_interval_ticks=_lt_capture,
                    activity_window_ticks=_lt_window,
                )
                _telemetry_store.set_dt_ms(_sim_dt_ms)
                # Prime Tick-0 frame so dashboard can respond immediately
                _telemetry_store.prime(controller.network)
                # Register post-tick hook via safe wrapper (routes errors to error buffer)
                from src.dashboard.live_projection import (
                    NetworkAccess as _NetworkAccess,
                )

                _hook: PostTickHook = make_telemetry_hook(
                    _telemetry_store, cast("_NetworkAccess", controller.network)
                )
                controller.add_hook(_hook)
                print(
                    f"✅ Live telemetry enabled (capture={_lt_capture}, window={_lt_window}, dt_ms={_sim_dt_ms})"
                )
            else:
                print("⚠️ Live telemetry disabled by config")

            _OperatorBridge_cls = cast(type, _OperatorBridge)
            operator_bridge = _OperatorBridge_cls(
                controller=controller,
                coordinator=_self_org_coordinator,
                plasticity=_self_org_plasticity,
                approval_policy=_self_org_approval_policy,
                telemetry_store=_telemetry_store,
            )
            # Attach the runtime config so the dashboard gate builder can
            # distinguish "disabled by config" from "config enabled but
            # component missing" (ERROR).
            operator_bridge.config_dict = config_dict  # type: ignore[attr-defined]
            print("✅ OperatorBridge created with canonical RuntimeController")

            _DashboardStateStore_cls = cast(type, _DashboardStateStore)
            state_store = _DashboardStateStore_cls()

            from src.dashboard.state import set_dashboard_config

            set_dashboard_config(config_dict)

            # Initialen Dashboard-Snapshot bei Tick 0 publizieren
            # (bevor der erste Tick ausgeführt wird, damit das Dashboard
            #  echte Netzwerkdaten anzeigt und nicht Nullen)
            from src.core.network import StepResult

            _initial_result = StepResult(
                tick=0,
                spike_ids=(),
                output_spike_ids=(),
                spikes_this_tick=0,
                total_spikes=0,
                delivered_events=0,
                queued_events=0,
                external_injection_count=0,
                external_total_current=0.0,
                synaptic_current_targets=0,
                mean_v=0.0,
                min_v=0.0,
                max_v=0.0,
                mean_energy=0.0,
                core_step_ms=0.0,
                neuron_activity={},
                total_synapses=network.synapse_count,
            )
            _publish_dashboard_state(
                state_store=state_store,
                network=network,
                result=_initial_result,
                learning=learning,
                homeostasis=homeostasis,
                storage_telemetry=_storage_telemetry,
                self_org_stats=_self_org_stats_func(),
                status="idle",
            )
            print("✅ Initial dashboard state published (Tick 0)")

        except Exception as e:
            print(f"⚠️ Dashboard setup failed: {type(e).__name__}: {e}")
            operator_bridge = None
            state_store = None

    # ================================================================
    # Artifacts (shared by hooks)
    # ================================================================
    artifacts_ctx = RunArtifacts(config_dict)
    artifacts = artifacts_ctx.__enter__()
    artifacts.save_topology(health)

    print(
        f"🧠 Neurons={len(network.neurons)} Synapses={network.synapse_count} "
        f"Input={len(network.input_cells)} Output={len(network.output_cells)} "
        f"Learning={'on' if learning and learning.enabled else 'off'} "
        f"Homeostasis={'on' if homeostasis and homeostasis.enabled else 'off'}"
        f"Controller={'idle' if controller else 'none'}"
    )

    # ================================================================
    # Dashboard im Hauptthread starten (falls aktiviert)
    # ================================================================
    if (
        not args.no_dashboard
        and _dashboard_available
        and operator_bridge is not None
        and state_store is not None
    ):
        try:
            # Write initial snapshot so the heatmap source has real data
            print("💾 Writing initial .b5d snapshot...")
            _write_snapshot()

            docs_root = Path("docs") if Path("docs").exists() else None
            research_root = Path("research") if Path("research").exists() else None

            _dashboard_host = args.dashboard_host
            _dashboard_port = args.dashboard_port
            print(
                f"🧠 Starting Brain-5D dashboard on http://{_dashboard_host}:{_dashboard_port}"
            )
            print("⏸️  Simulation starts in idle state. Use dashboard controls to run.")
            if _serve_dashboard is not None:
                _serve_dashboard(host=_dashboard_host, port=_dashboard_port, state=state_store, snapshot_path=_snapshot_path, structural_bridge=operator_bridge, docs_root=docs_root, research_root=research_root, chat_settings=cast(dict[str, Any], config_dict.get("research_chat", {})))  # type: ignore[reportOptionalCall, call-arg, operator]

        except KeyboardInterrupt:
            print("\n⏹️ Dashboard interrupted, stopping simulation...")
        finally:
            if controller is not None:
                controller.stop()
    else:
        # Kein Dashboard – starte Simulation mit konfigurierten Ticks
        total_ticks = int(config_dict.get("ticks", 1000))
        print(f"▶️  Running {total_ticks} ticks (no dashboard)...")
        if controller is not None:
            controller.run_ticks(total_ticks)
        else:
            print("⚠️ Controller not available, skipping simulation")
        # Write final snapshot
        print("💾 Writing final .b5d snapshot...")
        _write_snapshot()

    # ================================================================
    # Final summary
    # ================================================================
    report = propagation.get_report()
    summary: dict[str, Any] = {
        "seed": config_dict.get("seed", 42),
        "ticks": network.current_tick,
        "final_neurons": len(network.neurons),
        "final_synapses": network.synapse_count,
        "total_spikes": network.total_spikes,
        "topology": health,
        "propagation": asdict(report),
    }

    if learning and learning.enabled:
        summary["learning"] = asdict(learning.stats)
    if homeostasis and homeostasis.enabled:
        summary["homeostasis"] = asdict(homeostasis.stats)

    if core_times:
        ordered = sorted(core_times)
        p95 = ordered[max(0, int(len(ordered) * 0.95) - 1)]
        summary["benchmark"] = {
            "mean_ms": statistics.mean(core_times),
            "median_ms": statistics.median(core_times),
            "p95_ms": p95,
        }
        print("📊 Benchmark:", summary["benchmark"])

    artifacts.save_summary(summary)
    artifacts_ctx.__exit__(None, None, None)

    # Final dashboard state publication
    if state_store is not None:
        try:
            from src.dashboard.models import (
                DashboardSnapshot,
                HomeostasisMetrics,
                LearningMetrics,
                NetworkMetrics,
                SpikeMetrics,
                SystemMetrics,
            )

            learning_stats = learning.stats if learning is not None else None
            homeo_stats = homeostasis.stats if homeostasis is not None else None

            final_snapshot = DashboardSnapshot(
                status="completed",
                version=BRAIN5D_VERSION_DISPLAY,
                system=SystemMetrics(
                    tick=network.current_tick,
                    neurons=len(network.neurons),
                    synapses=network.synapse_count,
                    spikes_total=network.total_spikes,
                ),
                learning=LearningMetrics(
                    stdp_updates=getattr(learning_stats, "stdp_weight_updates", 0),
                    reward_updates=getattr(learning_stats, "reward_weight_updates", 0),
                    rewards_received=getattr(learning_stats, "rewards_received", 0),
                    rewards_applied=getattr(learning_stats, "rewards_applied", 0),
                    pending_rewards=getattr(learning_stats, "pending_rewards", 0),
                ),
                homeostasis=HomeostasisMetrics(
                    enabled=getattr(homeo_stats, "enabled", False),
                    target_rate_hz=getattr(homeo_stats, "target_rate_hz", 0.0),
                    actual_rate_hz=getattr(homeo_stats, "mean_rate_hz", 0.0),
                    rate_error_hz=getattr(homeo_stats, "mean_rate_error_hz", 0.0),
                    mean_rate_hz=getattr(homeo_stats, "mean_rate_hz", 0.0),
                    mean_rate_error_hz=getattr(homeo_stats, "mean_rate_error_hz", 0.0),
                    mean_threshold_adaptation=getattr(
                        homeo_stats, "mean_threshold_adaptation", 0.0
                    ),
                    target_energy=getattr(homeo_stats, "target_energy", 0.0),
                    mean_energy=getattr(homeo_stats, "mean_energy", 0.0),
                    mean_energy_error=getattr(homeo_stats, "mean_energy_error", 0.0),
                    active_neurons=getattr(homeo_stats, "active_neurons", 0),
                    updates=getattr(homeo_stats, "updates", 0),
                ),
                spikes=SpikeMetrics(
                    total_spikes=network.total_spikes,
                    spike_count_last_tick=0,
                ),
                network=NetworkMetrics(
                    tick=network.current_tick,
                    neuron_count=len(network.neurons),
                    synapse_count=network.synapse_count,
                ),
            )
            state_store.publish(final_snapshot)
        except Exception:
            pass

    print("\n📈 Propagation:", report)
    if learning and learning.enabled:
        print("📚 Learning:", learning.stats)
    if homeostasis and homeostasis.enabled:
        print("⚖️ Homeostasis:", homeostasis.stats)

    if observatory:
        print("🔭 Observatory running — close window to exit")
        observatory.block_until_closed()

    return 0


if __name__ == "__main__":
    raise SystemExit(main())