File size: 78,248 Bytes
c9fa753
 
 
 
 
 
 
 
 
 
 
931437f
 
c9fa753
1597754
bb286dd
1597754
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1597754
 
 
 
 
 
 
 
30b61f5
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931437f
 
 
 
 
 
 
30b61f5
 
 
971125f
 
 
 
 
931437f
 
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb286dd
 
 
 
 
c9fa753
1597754
30b61f5
 
 
c9fa753
bb286dd
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
931437f
 
 
c9fa753
1597754
c9fa753
 
 
 
 
 
 
 
 
 
 
1597754
c9fa753
931437f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9fa753
 
1597754
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1597754
931437f
 
 
 
c9fa753
 
931437f
 
c9fa753
931437f
 
c9fa753
931437f
 
c9fa753
931437f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9fa753
 
 
 
 
 
1597754
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30b61f5
 
 
c9fa753
 
 
931437f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9fa753
 
 
 
 
 
 
 
 
1597754
931437f
 
c9fa753
 
 
0dff983
 
 
 
 
 
 
c9fa753
bb286dd
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1597754
 
 
 
c9fa753
4e0c85a
2b5579d
 
 
 
 
 
 
33b81e9
c9fa753
 
 
6ef5a6f
 
c9fa753
 
 
 
 
 
 
 
 
 
 
8abec04
 
682d244
7e513ed
 
c9fa753
792a33c
c9fa753
 
 
 
 
8abec04
 
 
 
 
c9fa753
0dff983
 
c9fa753
 
 
30b61f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a91f00
30b61f5
 
 
 
 
 
971125f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30b61f5
 
 
 
 
 
 
 
 
 
 
 
 
c9fa753
 
 
 
931437f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9fa753
 
 
 
1597754
 
 
c9fa753
 
1597754
 
 
c9fa753
1597754
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931437f
 
 
c9fa753
 
931437f
 
 
 
c9fa753
1597754
ba553cc
 
 
 
 
 
 
 
 
 
 
 
 
1597754
 
ba553cc
 
 
 
 
1597754
 
 
ba553cc
30b61f5
 
 
 
 
 
 
 
 
c9fa753
 
30b61f5
 
 
 
 
 
 
 
 
 
 
c9fa753
 
 
 
 
 
 
 
da2ea0f
 
c9fa753
da2ea0f
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931437f
 
 
 
 
 
 
 
c9fa753
2b5579d
 
 
 
 
 
 
 
 
d092871
0ae86f3
 
 
 
 
2b5579d
 
0ae86f3
 
 
931437f
 
2b5579d
931437f
 
 
2b5579d
 
 
c9fa753
 
 
 
 
 
 
 
30b61f5
 
 
 
 
 
 
 
1a91f00
 
 
 
 
 
c9fa753
 
 
 
 
 
931437f
 
 
 
c9fa753
30b61f5
 
f954d8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
971125f
 
 
 
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
0bc5388
 
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
0bc5388
c9fa753
 
 
 
 
b4f6be8
 
138163a
 
 
30b61f5
138163a
 
 
d092871
 
138163a
c9fa753
 
 
0bc5388
 
 
30b61f5
 
 
 
 
 
 
 
 
0bc5388
30b61f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9517e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138163a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d092871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cddb1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d092871
 
 
 
 
 
 
 
 
 
 
 
 
30b61f5
 
 
 
 
0bc5388
 
 
 
30b61f5
 
0bc5388
30b61f5
 
 
d092871
 
30b61f5
 
 
 
 
 
 
 
 
0bc5388
30b61f5
 
 
 
f9517e4
 
138163a
 
 
e1704e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30b61f5
e1704e4
 
138163a
30b61f5
0bc5388
 
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
 
971125f
 
 
 
 
c9fa753
 
 
 
 
 
 
 
971125f
 
 
 
 
c9fa753
 
 
971125f
c9fa753
 
 
 
 
 
 
 
 
 
 
c3d0cb8
 
c9fa753
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Adapters: legacy outputs → AnalysisState.

These functions are the ONLY code that reads the legacy dicts (``all_data``, the forecast dict,
the fair-value dict, the story result, the debate dict, the blend dict …). Each writes the
namespaces of one pipeline stage. Everything downstream — personas, validators, the summary
prompt, the renderer, the claim audit and the recommendation — reads the state only.
"""
from __future__ import annotations

import datetime as _dt

import bridge as _br
import definitions as _defs
import forecast_state as _fs
from state import (ADJUSTED, ADS, CONSOLIDATED, CONTINUING, DERIVED, FY, FY1, FY2, GAAP, IFRS, INTRINSIC, MARKET, MODEL,
                   MODEL_ESTIMATE, NTM, ORDINARY_SHARE, PARTIALLY_RECONCILED, POINT, PRE_SEPARATION, RECONCILED, SPOT, TTM, UNAVAILABLE,
                   UNVERIFIED, VALIDATED, metric)


def _f(v):
    try:
        if v in (None, "", "None", "-"):
            return None
        x = float(v)
        return None if x != x else x
    except (TypeError, ValueError):
        return None


def _mv(metrics, k):
    return _f(((metrics or {}).get(k) or {}).get("value"))


def _today():
    return _dt.date.today().isoformat()


# ═══════════════════════════════════════════════════════════════════════════════════════
# DATA stage: identity, issuer, market, historical, consensus, provenance
# ═══════════════════════════════════════════════════════════════════════════════════════
def build_data_stage(state, all_data, price, metrics_pre=None, price_as_of=None, code_version=None, tech=None):
    d = all_data or {}
    ov = d.get("overview") or {}
    iss = d.get("issuer") or {}
    fw = d.get("valuation_framework") or {}
    pol = d.get("policy")
    rh = d.get("reconciled_historicals") or {}
    is_adr = bool(iss.get("is_adr"))
    adr_ratio = _f(iss.get("adr_ratio"))
    sb = ADS if is_adr else ORDINARY_SHARE
    basis = IFRS if "IFRS" in str(iss.get("accounting_standard") or "") else GAAP
    period_end = rh.get("period_end")
    # perimeter: a completed separation inside the trailing window (sell-side consensus already models
    # the smaller continuing company, confirmed by an 8-K) puts history and the forward view on
    # different businesses. Every historical metric carries that perimeter, so compatible() refuses
    # cross-perimeter growth rates, multiples and comparisons.
    rst = d.get("restatement") or {}
    separated = bool(rst) and str(rst.get("kind") or "") in ("spin_off", "divestiture", "sale", "split_off", "separation")
    hist_perimeter = PRE_SEPARATION if separated else CONSOLIDATED
    fwd_perimeter = CONTINUING if separated else CONSOLIDATED
    import corporate_actions as _ca
    perimeter_aid = _ca.action_id(state.identity.ticker, rst.get("kind"), rst.get("business")) if separated else None
    s = state
    s.identity.company_name = ov.get("Name") or s.identity.ticker
    s.identity.cik = iss.get("cik") or (d.get("submissions") or {}).get("cik")
    s.identity.as_of = _today()
    s.identity.code_version = code_version
    s.identity.sector = ov.get("Sector")
    s.identity.industry = ov.get("Industry")

    # issuer
    s.issuer.issuer_class = iss.get("issuer_class")
    s.issuer.filer_type = iss.get("filer_type")
    s.issuer.regime = "FOREIGN_PRIVATE_ISSUER" if iss.get("is_fpi") else "US_DOMESTIC"
    s.issuer.accounting_standard = iss.get("accounting_standard")
    s.issuer.reporting_currency = iss.get("reporting_currency")
    s.issuer.quote_currency = iss.get("quote_currency") or "USD"
    s.issuer.is_financial = bool(iss.get("is_financial"))
    s.issuer.is_fpi = bool(iss.get("is_fpi"))
    s.issuer.is_adr = is_adr
    s.issuer.adr_ratio = adr_ratio
    s.issuer.adr_source = iss.get("adr_source")
    s.issuer.security_basis = sb
    s.issuer.framework = {"name": fw.get("framework"), "reason": fw.get("valuation_framework_reason"),
                          "ev_meaningful": fw.get("ev_meaningful")}
    s.issuer.policy = ({"issuer_class": getattr(pol, "issuer_class", None),
                        "valuation_engine": getattr(pol, "valuation_engine", None),
                        "pricing_methods": list(getattr(pol, "allowed_pricing_methods", ()) or ()),
                        "disallowed_scorecard_rows": list(getattr(pol, "disallowed_scorecard_rows", ()) or ()),
                        "excluded_topics": list(getattr(pol, "persona_excluded_topics", ()) or ()),
                        "reason": getattr(pol, "reason", None)} if pol is not None else {})
    s.issuer.revenue_concept = (d.get("canonical_concepts") or {}).get("revenue")

    # segments: the canonical (reconciled) segment state is the only segment source for every consumer
    _canon = d.get("segments_canonical") or {"status": "UNAVAILABLE", "reason": "no segment data"}
    s.segments.canonical = _canon
    s.segments.status = _canon.get("status")
    # company guidance: primary source only (guidance.company_guidance); each verified row carries its rendering
    import catalog as _cat0
    _g = dict(d.get("company_guidance") or {"status": "UNAVAILABLE", "reason": "guidance not retrieved", "metrics": []})
    _g["metrics"] = [dict(m, rendered_value=m.get("rendered_value") or _cat0.render_guidance(m, "value_only"),
                          rendered_full=m.get("rendered_full") or _cat0.render_guidance(m, "full"))
                     for m in _g.get("metrics") or []]
    if _g.get("concept_states") is None:
        # a release that was never retrieved leaves every concept UNKNOWN (never an absence)
        import guidance as _gd
        _g["concept_states"] = _gd.concept_states(_g.get("metrics") or [], _g.get("rejected") or [],
                                                  release_checked=bool(_g.get("source")))
    s.guidance.company = _g
    s.guidance.status = _g.get("status")

    # market
    s.market.price = metric("price", "PRICE", price, SPOT, accounting_basis=MARKET, per_share=True,
                            security_basis=sb, period=price_as_of, source="Alpha Vantage global quote",
                            source_date=price_as_of, unit="USD")
    s.market.price_as_of = price_as_of
    mcap = _f(ov.get("MarketCapitalization"))
    s.market.market_cap = metric("market_cap", "MARKET_CAP", mcap, SPOT, accounting_basis=MARKET,
                                 source="Alpha Vantage company profile", source_date=ov.get("LatestQuarter"))
    sh = _f(ov.get("SharesOutstanding"))
    s.market.shares_outstanding = metric("shares_outstanding", "SHARES", sh, POINT, unit="count",
                                         security_basis=sb, source="Alpha Vantage company profile")
    s.market.shares_outstanding_sec = metric("shares_outstanding_sec", "SHARES", _mv(metrics_pre, "shares_outstanding_sec"),
                                             POINT, unit="count", security_basis=ORDINARY_SHARE,
                                             source="SEC cover page (XBRL dei)")
    dy = _mv(metrics_pre, "dividend_yield_statements")
    s.market.dividend_yield = metric("dividend_yield", "DIVIDEND_YIELD", dy, TTM, unit="ratio",
                                     derivation="dividends paid (cash-flow statements, TTM) ÷ market cap",
                                     quality_status=DERIVED if dy is not None else UNAVAILABLE)
    s.market.beta = _f(ov.get("Beta"))
    s.market.high_52w = metric("high_52w", "PRICE", _f(ov.get("52WeekHigh")), SPOT, per_share=True, security_basis=sb)
    s.market.low_52w = metric("low_52w", "PRICE", _f(ov.get("52WeekLow")), SPOT, per_share=True, security_basis=sb)
    mom = d.get("momentum_canon") or {}
    s.market.momentum = {"state": mom.get("label"), "detail": mom.get("detail"), "rule": mom.get("rule")}
    s.market.sma50 = metric("sma50", "PRICE", _f(ov.get("50DayMovingAverage")), SPOT, per_share=True, security_basis=sb)
    s.market.sma200 = metric("sma200", "PRICE", _f(ov.get("200DayMovingAverage")), SPOT, per_share=True, security_basis=sb)

    tsig = (tech or {}).get("signals") or {}
    s.market.return_3m = metric("return_3m", "PRICE_RETURN", _f(tsig.get("momentum_3m")), TTM, accounting_basis=MARKET, unit="ratio",
                                derivation="price change over 3 months (price history)")
    s.market.return_12m = metric("return_12m", "PRICE_RETURN", _f(tsig.get("momentum_12m")), TTM, accounting_basis=MARKET, unit="ratio",
                                 derivation="price change over 12 months (price history)")

    # historical (reconciled basis)
    # the TTM metrics' quality is their four quarters' (HistoricalMetricQuality), not the series-level status: a
    # series whose latest quarter could not be checked is partially reconciled, never simply "reconciled"
    _tq = (rh.get("ttm_quality") or {}).get("status")
    hq = {"SEC_RECONCILED": RECONCILED, "PARTIALLY_RECONCILED": PARTIALLY_RECONCILED}.get(
        _tq, RECONCILED if (_tq is None and rh.get("status") in ("RECONCILED", "SEC_ONLY")) else UNVERIFIED)
    hkw = dict(period_type=TTM, accounting_basis=basis, period=period_end, source=rh.get("basis") or "provider statements",
               quality_status=hq, perimeter=hist_perimeter)
    s.historical.perimeter = {"historical": hist_perimeter, "forward": fwd_perimeter, "action_id": perimeter_aid,
                              "event": (dict({k: rst.get(k) for k in ("kind", "business", "effective", "revenue_share", "basis")},
                                             action_id=perimeter_aid) if separated else None)}
    s.historical.reconciliation = {k: rh.get(k) for k in ("status", "basis", "period_end", "overrides", "disagreements",
                                                          "unverified_quarters", "candidates", "user_limitations", "pipeline_notes", "sec",
                                                          "ttm_quality")}
    s.historical.revenue_ttm = metric("revenue_ttm", "REVENUE", rh.get("revenue_ttm"), unit="USD", **hkw)
    s.historical.operating_income_ttm = metric("operating_income_ttm", "OPERATING_INCOME", rh.get("operating_income_ttm"), unit="USD", **hkw)
    s.historical.pretax_income_ttm = metric("pretax_income_ttm", "PRETAX_INCOME", rh.get("pretax_income_ttm"), unit="USD", **hkw)
    s.historical.net_income_ttm = metric("net_income_ttm", "NET_INCOME", rh.get("net_income_ttm"), unit="USD", **hkw)
    s.historical.operating_margin_ttm = metric("operating_margin_ttm", "OPERATING_MARGIN", rh.get("operating_margin_ttm"),
                                               unit="ratio", derivation="operating income ÷ revenue, reconciled TTM", **hkw)
    s.historical.net_margin_ttm = metric("net_margin_ttm", "NET_MARGIN", rh.get("net_margin_ttm"), unit="ratio",
                                         derivation="net income ÷ revenue, reconciled TTM", **hkw)
    # EPS: three roles, each labelled; use cases select via SELECTION_POLICY
    q_e = ((d.get("earnings") or {}).get("quarterlyEarnings") or [])[:4]
    adj = [_f(q.get("reportedEPS")) for q in q_e]
    eps_adj = sum(adj) if len(adj) == 4 and all(v is not None for v in adj) else None
    s.historical.eps_adjusted_ttm = metric("eps_adjusted_ttm", "EPS", eps_adj, TTM, accounting_basis=ADJUSTED,
                                           per_share=True, security_basis=sb, period=(q_e[0].get("fiscalDateEnding") if q_e else None),
                                           period_end=(q_e[0].get("fiscalDateEnding") if q_e else None),
                                           adjustment_definition_id=_defs.PROVIDER_REPORTED_EPS, adjustment_provider="Alpha Vantage",
                                           adjustment_version=_defs.PROVIDER_REPORTED_EPS,
                                           source="Alpha Vantage EARNINGS reportedEPS (provider / sell-side basis)",
                                           derivation="Σ last four reported quarterly EPS", perimeter=hist_perimeter,
                                           quality_status=VALIDATED if eps_adj is not None else UNAVAILABLE)
    sec_eps = ((rh.get("candidates") or {}).get("sec_ttm") or {}).get("eps_diluted")
    sec_end = ((rh.get("candidates") or {}).get("sec_ttm") or {}).get("period_end")
    gaap_eps, gaap_src = None, None
    if sec_eps is not None and period_end and sec_end and abs((_dt.date.fromisoformat(sec_end[:10]) - _dt.date.fromisoformat(str(period_end)[:10])).days) <= 10:
        gaap_eps = sec_eps * (adr_ratio if (is_adr and adr_ratio) else 1.0)
        gaap_src = "SEC XBRL diluted EPS, trailing twelve months"
    elif rh.get("net_income_ttm") is not None and sh:
        gaap_eps = rh["net_income_ttm"] / sh
        gaap_src = "reconciled TTM net income ÷ shares outstanding"
    s.historical.eps_gaap_ttm = metric("eps_gaap_ttm", "EPS", gaap_eps, TTM, accounting_basis=basis, per_share=True,
                                       security_basis=sb, period=period_end, source=gaap_src or "", perimeter=hist_perimeter,
                                       quality_status=VALIDATED if gaap_eps is not None else UNAVAILABLE)
    # the adjusted↔GAAP bridge, like for like (bridge.py), and the canonical diluted share count
    _ser = rh.get("sec_series") or {}
    _q_all = ((d.get("earnings") or {}).get("quarterlyEarnings") or [])[:28]
    _eps_rows = [dict(r, value=r["value"] * (adr_ratio if (is_adr and adr_ratio) else 1.0)) for r in (_ser.get("eps_diluted") or [])]
    bridge = _br.build(_q_all, _eps_rows, ttm_adjusted=eps_adj, ttm_gaap=gaap_eps, ttm_end=period_end)
    s.historical.adjustment_bridge = bridge
    if bridge.get("methodology_change"):
        # the trailing adjusted EPS spans the old definition only partly: it is labelled with the new version
        s.historical.eps_adjusted_ttm = s.historical.eps_adjusted_ttm.with_value(
            s.historical.eps_adjusted_ttm.value, adjustment_version=bridge.get("definition_version"))
    dsh, dsh_src = _br.latest_diluted_shares(_ser.get("diluted_shares"))
    if dsh and is_adr and adr_ratio:
        dsh, dsh_src = dsh / adr_ratio, (dsh_src or "") + f" ÷ {adr_ratio:g} ordinary shares per ADS"
    if not dsh and rh.get("net_income_ttm") and gaap_eps and gaap_eps > 0:
        dsh, dsh_src = rh["net_income_ttm"] / gaap_eps, "trailing GAAP net income ÷ trailing diluted EPS (weighted diluted count)"
    dsh_q = DERIVED
    if not dsh:
        dsh, dsh_src, dsh_q = sh, "basic shares outstanding (no diluted count available)", UNVERIFIED
    s.historical.diluted_shares = metric("diluted_shares", "DILUTED_SHARES", dsh, POINT, unit="count", security_basis=sb,
                                         source=dsh_src or "", quality_status=dsh_q if dsh else UNAVAILABLE)
    _bq = ((d.get("balance") or {}).get("quarterlyReports") or [{}])[0] or {}
    _eq = _f(_bq.get("totalShareholderEquity"))
    s.historical.book_equity = metric("book_equity", "BOOK_EQUITY", _eq, POINT, accounting_basis=basis, unit="USD",
                                      period=_bq.get("fiscalDateEnding"), source="latest quarterly balance sheet (provider statements)")
    _ni = rh.get("net_income_ttm")
    s.historical.roe_ttm = metric("roe_ttm", "ROE", (_ni / _eq) if (_ni is not None and _eq and _eq > 0) else None, TTM,
                                  accounting_basis=basis, unit="ratio", derivation="reconciled TTM net income ÷ latest book equity",
                                  quality_status=DERIVED if (_ni is not None and _eq and _eq > 0) else UNAVAILABLE)
    _ta = _f(_bq.get("totalAssets"))
    s.historical.roa_ttm = metric("roa_ttm", "ROA", (_ni / _ta) if (_ni is not None and _ta and _ta > 0) else None, TTM,
                                  accounting_basis=basis, unit="ratio", derivation="reconciled TTM net income ÷ latest total assets",
                                  period=_bq.get("fiscalDateEnding"),
                                  quality_status=DERIVED if (_ni is not None and _ta and _ta > 0) else UNAVAILABLE)
    s.historical.eps_provider_ttm = metric("eps_provider_ttm", "EPS", _f(ov.get("EPS")), TTM, accounting_basis="PROVIDER",
                                           per_share=True, security_basis=sb, source="Alpha Vantage company profile",
                                           source_date=ov.get("LatestQuarter"), quality_status=UNVERIFIED, perimeter=hist_perimeter)
    s.historical.revenue_fy_sec = metric("revenue_fy_sec", "REVENUE", _mv(metrics_pre, "revenue_fy_sec"), FY,
                                         accounting_basis=basis, source="SEC XBRL company facts")
    surp = [_f(q.get("surprisePercentage")) for q in ((d.get("earnings") or {}).get("quarterlyEarnings") or [])[:8]]
    surp = [x for x in surp if x is not None]
    s.historical.beat_rate_8q = metric("beat_rate_8q", "BEAT_RATE", (sum(1 for x in surp if x > 0) / len(surp)) if surp else None,
                                       TTM, unit="ratio", derivation=f"share of the last {len(surp)} quarters with a positive surprise",
                                       quality_status=DERIVED if surp else UNAVAILABLE)
    s.historical.beat_window = len(surp)
    s.historical.statements_as_of = period_end
    s.historical.profile_as_of = ov.get("LatestQuarter")

    # consensus (already FX-normalised at the boundary)
    ee = d.get("earnings_estimates_consensus") or {}
    n = int(_f(ee.get("analyst_count")) or 0)
    cq = VALIDATED if n else UNAVAILABLE
    ckw = dict(accounting_basis=ADJUSTED, source="Alpha Vantage EARNINGS_ESTIMATES", quality_status=cq, perimeter=fwd_perimeter)
    edef = dict(adjustment_definition_id=_defs.SELL_SIDE_CONSENSUS_EPS, adjustment_provider="Alpha Vantage",
                adjustment_version=_defs.SELL_SIDE_CONSENSUS_EPS)
    fy1_s, fy1_e = _defs.fiscal_year_bounds(ee.get("current_year_date"))
    fy2_s, fy2_e = _defs.fiscal_year_bounds(ee.get("next_year_date"))
    s.consensus.n_analysts = n
    s.consensus.eps_fy1 = metric("consensus_eps_fy1", "EPS", _f(ee.get("current_year_eps")) if n else None, FY1,
                                 per_share=True, security_basis=sb, period=ee.get("current_year_date"),
                                 period_start=fy1_s, period_end=fy1_e, **edef, **ckw)
    s.consensus.eps_fy2 = metric("consensus_eps_fy2", "EPS", _f(ee.get("next_year_eps")) if n else None, FY2,
                                 per_share=True, security_basis=sb, period=ee.get("next_year_date"),
                                 period_start=fy2_s, period_end=fy2_e, **edef, **ckw)
    s.consensus.revenue_fy1 = metric("consensus_revenue_fy1", "REVENUE", _f(ee.get("current_year_revenue")) if n else None,
                                     FY1, period=ee.get("current_year_date"), period_start=fy1_s, period_end=fy1_e,
                                     **{**ckw, "accounting_basis": basis})
    s.consensus.revenue_fy2 = metric("consensus_revenue_fy2", "REVENUE", _f(ee.get("next_year_revenue")) if n else None,
                                     FY2, period=ee.get("next_year_date"), period_start=fy2_s, period_end=fy2_e,
                                     **{**ckw, "accounting_basis": basis})
    # the explicit period conversion: consensus for the SAME twelve months the engine forecasts
    _w = _defs.ntm_weights(period_end, fy1_s, fy1_e, fy2_s, fy2_e) if n else None
    for _key, _c1, _c2, _concept, _bk in (("eps_ntm", s.consensus.eps_fy1, s.consensus.eps_fy2, "EPS", "eps"),
                                          ("revenue_ntm", s.consensus.revenue_fy1, s.consensus.revenue_fy2, "REVENUE", "rev")):
        _v, _der = None, None
        if _w and _c1.value is not None and (_c2.value is not None or _w[1] < 1e-9):
            _v = _w[0] * _c1.value + _w[1] * (_c2.value or 0.0)
            _der = (f"time-weighted consensus for the twelve months after {period_end}: {_w[0]:.0%} of FY1 "
                    f"(ending {fy1_e}) + {_w[1]:.0%} of FY2 (ending {fy2_e})")
        _extra = dict(per_share=True, security_basis=sb, **edef) if _bk == "eps" else {"accounting_basis": basis}
        setattr(s.consensus, _key, metric(f"consensus_{_key}", _concept, _v, NTM, period_start=(_w[2] if _w else None),
                                          period_end=(_w[3] if _w else None), derivation=_der,
                                          **{**ckw, **({"quality_status": DERIVED} if _v is not None else {"quality_status": UNAVAILABLE}),
                                             **_extra}))
    s.consensus.target_price = metric("analyst_target", "TARGET_PRICE", _f(ov.get("AnalystTargetPrice")), POINT,
                                      accounting_basis=MARKET, per_share=True, security_basis=sb,
                                      source="Alpha Vantage company profile (mean sell-side target)")
    s.consensus.n_ratings = sum(int(_f(ov.get(k)) or 0) for k in ("AnalystRatingStrongBuy", "AnalystRatingBuy",
                                                                   "AnalystRatingHold", "AnalystRatingSell", "AnalystRatingStrongSell"))
    s.consensus.normalization = ee.get("_normalization") or []
    s.consensus.reconciliation_notes = list(rh.get("consensus_notes") or [])

    # provenance
    s.provenance.sources = {
        "price": {"source": "Alpha Vantage", "as_of": price_as_of},
        "statements": {"source": rh.get("basis"), "as_of": period_end},
        "profile": {"source": "Alpha Vantage company profile", "as_of": ov.get("LatestQuarter"),
                    "note": "cross-check only; its derived fields are replaced by the reconciled basis"},
        "consensus": {"source": "Alpha Vantage EARNINGS_ESTIMATES", "n": n},
        "sec": {"available": bool((rh.get("sec") or {}).get("available")), "latest_ttm_end": sec_end},
    }
    s.provenance.fx = {"rate_to_usd": _f(d.get("fx_usd")), "source": d.get("fx_source")}
    _t10 = d.get("treasury_10y") or []
    _rf = _f(_t10[0].get("value")) if (isinstance(_t10, list) and _t10 and isinstance(_t10[0], dict)) else None
    s.provenance.risk_free = {"rate": (_rf / 100.0) if _rf is not None else None,
                              "source": "Alpha Vantage TREASURY_YIELD 10y (daily)" if _rf is not None else None,
                              "as_of": (_t10[0].get("date") if _rf is not None else None)}
    # every metric on a separated perimeter names the corporate action that split it
    from state import stamp_perimeter as _stamp
    _stamp(s, s.historical.perimeter.get("action_id"), ("historical", "consensus", "market"))
    return state


def reconcile_forecast_from_state(s, pred_fund):
    """The single reconciliation stage (forecast_state.reconcile_forecast) fed only from canonical state:
    reconciled history, the adjusted↔GAAP bridge, the canonical diluted share count, perimeter and period."""
    pf = pred_fund or {}
    hist = {k: s.historical.get(k) for k in ("revenue_ttm", "operating_income_ttm", "pretax_income_ttm",
                                              "net_income_ttm", "eps_adjusted_ttm")}
    per = s.historical.get("perimeter") or {}
    dsh = s.historical.get("diluted_shares")
    pf["_diluted_shares_source"] = getattr(dsh, "source", None)
    return _fs.reconcile_forecast(pf, hist, shares=s.value("market.shares_outstanding"),
                                  bridge=dict(s.historical.get("adjustment_bridge") or {}),
                                  diluted_shares=getattr(dsh, "value", None), is_financial=s.issuer.is_financial,
                                  accounting_standard=s.issuer.accounting_standard, per_share_basis=s.issuer.security_basis,
                                  period_end=s.historical.get("statements_as_of"),
                                  perimeter=per.get("forward") or CONSOLIDATED, base_perimeter=per.get("historical") or CONSOLIDATED)


# ═══════════════════════════════════════════════════════════════════════════════════════
# FORECAST / VALUATION stage
# ═══════════════════════════════════════════════════════════════════════════════════════
def build_forecast_stage(state, *, pred_fund, fair_val, story, engine_run, engine_meta, tech, fund, sent,
                         macro, ml, quant_rel, panel_rel, panel_row, all_data):
    s = state
    d = all_data or {}
    sb = s.issuer.security_basis
    pf = pred_fund or {}
    per = s.historical.get("perimeter") or {}
    # the ReconciledForecastState pricing already consumed (one object), else reconciled here from the same inputs
    fs = pf.get("_reconciled") or reconcile_forecast_from_state(s, pf)
    for k, v in fs.items():
        setattr(s.forecast, k, v)
    s.forecast.identities = [{"name": n, "ok": ok, "detail": det} for n, ok, det in _fs.identity_check(fs)]
    # the change a persona wants to state, as a claim (trailing → forecast), only on one basis and perimeter
    from state import compatible as _cmp
    _om_f, _om_h = s.forecast.get("operating_margin"), s.historical.get("operating_margin_ttm")
    s.forecast.operating_margin_change = (
        (_om_f.value - _om_h.value) if (_om_f is not None and _om_h is not None and _om_f.value is not None
                                        and _om_h.value is not None and _cmp(_om_f, _om_h, "growth")[0]
                                        and (per.get("forward") or CONSOLIDATED) == (per.get("historical") or CONSOLIDATED)) else None)
    s.forecast.consensus_gates = [g for g in (pf.get("eps_consensus_gate"), pf.get("rev_consensus_gate")) if g]
    # typed lineage: the share of each forecast that is the model's own (model_weight 0 = the forecast IS consensus)
    s.forecast.consensus_anchor = {"eps": pf.get("eps_consensus_anchor"), "revenue": pf.get("rev_consensus_anchor")}

    fv = fair_val or {}
    st = story or {}
    em = engine_meta or {}
    er = engine_run or {}
    # valuation engine: the ONLY source of valuation vocabulary
    s.valuation.engine = {k: em.get(k) for k in ("engine_id", "engine_family", "display_name", "short_display_name",
                                                  "methodology_description", "label_intrinsic", "terms",
                                                  "allowed_renderer_terms", "forbidden_renderer_terms")}
    s.valuation.engine_status = er.get("status")
    s.valuation.engine_components = er.get("components") or []
    story_value = _f(st.get("value"))
    mech = _f(((fv.get("methods") or {}).get("DCF") or {}).get("price"))
    fam = em.get("engine_family")
    # intrinsic value: the selected engine decides. For the FCFF engine the story value is the
    # intrinsic estimate and the mechanical DCF is its cross-check; other engines publish their own.
    if fam == "intrinsic_cashflow":
        intrinsic, same_model = (story_value, False) if story_value else (mech, True)
        intrinsic_src = "story-driven FCFF (median storyteller)" if story_value else "mechanical earnings DCF (no story ran)"
    else:
        intrinsic = _f(er.get("value")) if er.get("status") == "OK" else None
        same_model = True
        intrinsic_src = em.get("display_name") or "valuation engine"
    s.valuation.intrinsic_value = metric("intrinsic_value", "INTRINSIC_VALUE", intrinsic, INTRINSIC, accounting_basis=MODEL,
                                         per_share=True, security_basis=sb, source=intrinsic_src,
                                         quality_status=MODEL_ESTIMATE if intrinsic else UNAVAILABLE)
    s.valuation.mechanical_value = metric("mechanical_value", "INTRINSIC_VALUE", mech if not same_model else None, INTRINSIC,
                                          accounting_basis=MODEL, per_share=True, security_basis=sb,
                                          source="mechanical earnings DCF", quality_status=MODEL_ESTIMATE)
    s.valuation.same_model = same_model
    # the words for the intrinsic figure follow the model that actually produced it
    _terms = em.get("terms") or {}
    s.valuation.intrinsic_label = (_terms.get("intrinsic") if (fam != "intrinsic_cashflow" or story_value)
                                   else (_terms.get("cross_check") or "mechanical earnings DCF")) or "intrinsic value"
    s.valuation.lifecycle_stage = fv.get("lifecycle_stage")
    s.valuation.damodaran_inputs = dict(d.get("damodaran_inputs") or {"available": False})
    lb = fv.get("lifecycle_basis") or {}
    s.valuation.lifecycle = {"stage": fv.get("lifecycle_stage"),
                             "trailing_revenue_growth": _f(lb.get("trailing_revenue_growth")),
                             "forward_revenue_growth": _f(lb.get("forward_revenue_growth")),
                             "forward_eps_growth": _f(lb.get("forward_eps_growth")),
                             "forward_source": lb.get("forward_source"),
                             "basis": "Damodaran corporate life cycle: revenue growth (trailing and forward), profitability, payout"}
    s.valuation.gaap_distortion = dict(fv.get("stage_check") or {})      # deterministic test; see composite steps
    dm = fv.get("dcf_meta") or {}
    s.valuation.mechanical_inputs = {k: dm.get(k) for k in ("basis", "rf", "erp", "beta", "ke_initial", "g1", "g_terminal",
                                                            "high_growth_years", "roe")}
    # MechanicalDCFState (valuation_engines.two_stage_per_share): everything the mechanical value reproduces from
    s.valuation.mechanical_dcf = dm.get("dcf_state")
    stories = list(st.get("stories") or [])
    A = st.get("assumptions") or {}
    s.valuation.story = {
        "ran": bool(story_value),
        "stories": stories,
        "n_storytellers": len(stories),
        "adopted": next((x.get("index") for x in stories if x.get("adopted")), None),
        "aggregation": st.get("consensus_method"),
        "assumptions": {k: A.get(k) for k in ("tam_usd_b", "peak_share", "revenue_N_usd_b", "implied_rev_cagr", "growth_stage1",
                                              "stage1_years", "years", "margin_now", "margin_target", "sales_to_capital",
                                              "failure_probability", "wacc_initial", "wacc_terminal", "terminal_growth",
                                              "terminal_roc", "pv_terminal_share", "revenue_reported_usd_b", "revenue_start_usd_b",
                                              "revenue_base_note", "margin_base_note", "cost_of_debt",
                                              "debt_weight", "beta", "beta_stable", "wacc_terminal_basis", "wacc_hold_years", "stable_rate_kept",
                                              "defaults_used", "discount_rate_path", "margin_proposed", "margin_cap",
                                              "margin_floor")},
        "segments": A.get("segments") or [],
        "segment_build": dict((d.get("segment_brief") or {}).get("required_evidence") or {"required": False}),
        "dispersion": st.get("dispersion"),
        "breakeven": st.get("breakeven"),
        "bridge": st.get("bridge"),
        "text": st.get("text") or st.get("story"),
        "key_risk": st.get("key_risk"),
        # the revenue the FCFF actually starts from (the continuing company after a separation)
        "start_revenue": ((A.get("revenue_start_usd_b") or A.get("revenue_reported_usd_b")) * 1e9
                          if (A.get("revenue_start_usd_b") or A.get("revenue_reported_usd_b")) is not None else None),
        "revenue_base_note": A.get("revenue_base_note"),
        "margin_base_note": A.get("margin_base_note"),
        "start_margin": A.get("margin_now"),
        "margin_change": ((A["margin_target"] - A["margin_now"])
                          if (A.get("margin_target") is not None and A.get("margin_now") is not None) else None),
    }
    s.valuation.sensitivity = [dict(x) for x in (st.get("sensitivity") or [])]

    # ── typed claims: no material financial float reaches a persona without concept, period and perimeter ──
    _per = s.historical.get("perimeter") or {}
    _hp, _fp, _aid = _per.get("historical") or CONSOLIDATED, _per.get("forward") or CONSOLIDATED, _per.get("action_id")
    s.valuation.lifecycle_trailing_growth = metric(
        "revenue_growth_ttm", "REVENUE_GROWTH", _f(lb.get("trailing_revenue_growth")), TTM, accounting_basis=GAAP, unit="ratio",
        display_unit="PERCENT", perimeter=_hp, period=s.historical.get("statements_as_of"),
        source="reconciled quarterly revenue, trailing four quarters over the four before",
        derivation="trailing twelve months ÷ the prior twelve months − 1", quality_status=DERIVED)
    s.valuation.lifecycle_forward_growth = metric(
        "revenue_growth_fy2", "REVENUE_GROWTH", _f(lb.get("forward_revenue_growth")), FY2, accounting_basis=GAAP, unit="ratio",
        display_unit="PERCENT", perimeter=_fp, source=str(lb.get("forward_source") or "sell-side consensus"),
        derivation="consensus revenue, next fiscal year ÷ this fiscal year − 1", quality_status=DERIVED)
    _di0 = s.valuation.get("damodaran_inputs") or {}
    s.valuation.industry_operating_margin = metric(
        "industry_operating_margin", "OPERATING_MARGIN", _f(_di0.get("industry_operating_margin")), TTM, accounting_basis=GAAP,
        unit="ratio", display_unit="PERCENT", scope="INDUSTRY", perimeter="industry aggregate",
        source=f"Damodaran margin dataset: pre-tax unadjusted operating margin, {_di0.get('industry')} ({_di0.get('region')}, "
               f"{_di0.get('industry_as_of')})", quality_status=VALIDATED if _di0.get("industry_operating_margin") is not None else UNAVAILABLE)
    # StoryBase: the business the story values, over one stated period (story_base.py)
    _sb = d.get("story_base") or {}
    _skw = dict(period_type=TTM, accounting_basis=GAAP, perimeter=_sb.get("perimeter") or CONSOLIDATED,
                period=_sb.get("period_end"), period_start=_sb.get("period_start"), period_end=_sb.get("period_end"),
                corporate_action_id=_sb.get("corporate_action_id"), derivation=_sb.get("derivation"),
                quality_status=_sb.get("quality_status") or UNAVAILABLE)
    s.valuation.story_base = {
        "revenue": metric("story_base_revenue", "REVENUE", _sb.get("revenue"), unit="USD", **_skw),
        "operating_income": metric("story_base_operating_income", "OPERATING_INCOME", _sb.get("operating_income"), unit="USD", **_skw),
        "operating_margin": metric("story_base_operating_margin", "OPERATING_MARGIN", _sb.get("operating_margin"), unit="ratio",
                                   display_unit="PERCENT", **_skw),
        "label": _sb.get("label") or "reconciled trailing operating margin", "revenue_label": _sb.get("revenue_label"),
        "perimeter": _sb.get("perimeter"), "corporate_action_id": _sb.get("corporate_action_id"),
        "perimeter_id": __import__("business").perimeter_id(_sb),
        "quality_status": _sb.get("quality_status"), "derivation": _sb.get("derivation"), "cross_check": _sb.get("cross_check")}
    s.valuation.perimeter_adjustment = dict(d.get("perimeter_adjustment") or {})
    s.valuation.story_maturity_margin = metric(
        "story_maturity_margin", "OPERATING_MARGIN", _f(A.get("margin_target")), INTRINSIC, accounting_basis=MODEL, unit="ratio",
        display_unit="PERCENT", perimeter=_sb.get("perimeter") or CONSOLIDATED, corporate_action_id=_sb.get("corporate_action_id"),
        source="adopted storyteller (after guardrails)", quality_status=MODEL_ESTIMATE if A.get("margin_target") is not None else UNAVAILABLE)
    # story assumptions and the break-even solve, typed: no raw financial float reaches a persona
    _tkw = dict(period_type=INTRINSIC, accounting_basis=MODEL, perimeter=_sb.get("perimeter") or CONSOLIDATED,
                corporate_action_id=_sb.get("corporate_action_id"))
    _bn = lambda x: (float(x) * 1e9) if x is not None else None
    s.valuation.story_revenue_maturity = metric(
        "story_revenue_maturity", "REVENUE", _bn(A.get("revenue_N_usd_b")), unit="USD", source="adopted storyteller",
        derivation="market size at maturity × peak share", quality_status=MODEL_ESTIMATE if A.get("revenue_N_usd_b") else UNAVAILABLE, **_tkw)
    s.valuation.story_tam = metric("story_tam", "MARKET_SIZE", _bn(A.get("tam_usd_b")), unit="USD", source="adopted storyteller",
                                   quality_status=MODEL_ESTIMATE if A.get("tam_usd_b") else UNAVAILABLE, **_tkw)
    s.valuation.story_share = metric("story_share", "MARKET_SHARE", _f(A.get("peak_share")), unit="ratio", display_unit="PERCENT",
                                     source="adopted storyteller", quality_status=MODEL_ESTIMATE, **_tkw)
    s.valuation.story_years = metric("story_years", "YEARS", _f(A.get("years")), unit="count", currency=None,
                                     source="adopted storyteller", quality_status=MODEL_ESTIMATE, **_tkw)
    _be = st.get("breakeven") or {}
    _bm = _be.get("story_margin")
    s.valuation.breakeven_revenue = metric(
        "breakeven_revenue", "BREAK_EVEN_REVENUE", _bn(_be.get("revenue_needed_N_usd_b")) if _be.get("revenue_needed_reachable") else None,
        unit="USD", source="break-even solve (story DCF = today's price)",
        derivation=f"revenue at maturity at which the story DCF equals the price, other story assumptions held"
                   + (f" (operating margin {float(_bm) * 100:.1f}%)" if _bm is not None else ""),
        quality_status=MODEL_ESTIMATE if _be.get("revenue_needed_reachable") else UNAVAILABLE, **_tkw)
    s.valuation.breakeven_margin = metric(
        "breakeven_margin", "BREAK_EVEN_OPERATING_MARGIN", _f(_be.get("margin_needed")) if _be.get("margin_needed_reachable") else None,
        unit="ratio", display_unit="PERCENT", source="break-even solve (story DCF = today's price)",
        derivation="operating margin at maturity at which the story DCF equals the price on the story's revenue path",
        quality_status=MODEL_ESTIMATE if _be.get("margin_needed_reachable") else UNAVAILABLE, **_tkw)
    s.valuation.breakeven_revenue_cagr = metric(
        "breakeven_revenue_cagr", "BREAK_EVEN_REVENUE_CAGR", _f(_be.get("rev_cagr_needed")) if _be.get("revenue_needed_reachable") else None,
        unit="ratio", display_unit="PERCENT", source="break-even solve (story DCF = today's price)",
        derivation="(breakeven_revenue / story_base_revenue) ^ (1 / story_years) − 1",
        quality_status=MODEL_ESTIMATE if _be.get("revenue_needed_reachable") else UNAVAILABLE, **_tkw)
    # the CAGR is bound to ONE pair: it measures growth from the starting revenue to the break-even revenue
    s.valuation.breakeven_growth_binding = {"start_revenue_metric_id": "story_base_revenue",
                                            "target_revenue_metric_id": "breakeven_revenue", "years_metric_id": "story_years"}
    # DiscountRatePath: the exact vector the valuation discounted with (story_dcf), and its claims
    _dp = dict(A.get("discount_rate_path") or {})
    _ckw = dict(period_type=INTRINSIC, accounting_basis=MODEL, unit="ratio", display_unit="PERCENT", quantity_type="YIELD",
                perimeter=_sb.get("perimeter") or CONSOLIDATED, corporate_action_id=_sb.get("corporate_action_id"))
    s.valuation.discount_rate_path = dict(
        _dp, initial_wacc_metric=metric("initial_wacc", "COST_OF_CAPITAL", _dp.get("initial_wacc"), derivation="rf + beta × ERP, "
                                        "with the company's debt ratio and synthetic cost of debt", source="story valuation", **_ckw),
        terminal_wacc_metric=metric("terminal_wacc", "COST_OF_CAPITAL", _dp.get("terminal_wacc"), derivation=_dp.get("rule"),
                                    source="story valuation (the rate used in the terminal value)", **_ckw),
        benchmark_wacc_metric=metric("mature_benchmark_wacc", "COST_OF_CAPITAL", _dp.get("mature_benchmark_wacc"),
                                     derivation="Damodaran Total Market (without financials) rebuilt at today's rf and ERP",
                                     source="Damodaran datasets (benchmark; used only if the rule selects it)", **_ckw))

    # pricing
    comp = _f(fv.get("fair_price"))
    s.pricing.market_composite = metric("market_composite", "MARKET_PRICING", comp, POINT, accounting_basis=MARKET,
                                        per_share=True, security_basis=sb, source="multiples and analyst targets")
    # PricingState: every method with its input, multiple, basis, weights and adjustments; raw → trim → cap → final
    import pricing_state as _pst
    ps = _pst.build(fv, s.value("market.price"))
    s.pricing.state = ps
    s.pricing.methods = [{"name": m["method"], "label": m["label"], "price": _f(m.get("raw_price")),
                          "share": _f(m.get("share_final")), "share_raw": _f(m.get("share_raw")),
                          "confidence": _f(m.get("confidence")), "stage_weight": _f(m.get("stage_weight")),
                          "effective_weight": _f(m.get("effective_weight")), "target_multiple": _f(m.get("target_multiple")),
                          "multiple_basis": m.get("multiple_basis"), "raw_input": _f(m.get("raw_input")),
                          "raw_input_basis": m.get("raw_input_basis"), "in_composite": bool(m.get("in_composite")),
                          "dropped_reason": m.get("dropped_reason"), "adjustments": list(m.get("adjustments") or [])}
                         for m in ps["methods"] if m["method"] != "DCF"]
    s.pricing.composite_steps = [dict(x) for x in ps["steps"]]
    _pkw = dict(accounting_basis=MARKET, per_share=True, security_basis=sb, unit="USD")
    s.pricing.raw_composite = metric("raw_composite", "MARKET_PRICING", _f(ps.get("raw_composite")), POINT,
                                     derivation="Σ method price × (confidence × lifecycle weight) ÷ Σ weights", **_pkw)
    s.pricing.post_trim_composite = metric("post_trim_composite", "MARKET_PRICING", _f(ps.get("post_trim_composite")), POINT,
                                           derivation="raw composite after outlier trimming", **_pkw)
    _cap = ps.get("cap") or {}
    s.pricing.cap = {"applied": bool(_cap.get("applied")), "upper": _f(_cap.get("upper")), "lower": _f(_cap.get("lower"))}
    _atm = next((m for m in ps["methods"] if m["method"] == "Analyst Target"), None)
    s.pricing.analyst_target_adjusted = metric(
        "analyst_target_adjusted", "TARGET_PRICE", _f((_atm or {}).get("risk_adjusted_analyst_target_contribution")), POINT,
        derivation="mean target with 55% of its implied upside kept (ANALYST_TARGET_OPTIMISM)", **_pkw)
    s.pricing.analyst_target_in_composite = bool(_atm and _atm.get("in_composite"))
    s.pricing.adjustments = [dict(a) for a in ps["adjustments"]]
    eps_f = s.forecast.eps
    eps_a = s.select("TRAILING_PE_DENOMINATOR")
    eps_g = s.select("GAAP_PE_DENOMINATOR")
    pm = s.market.price
    from state import compatible as _compat0
    pol_methods = set((s.issuer.get("policy") or {}).get("pricing_methods") or ())
    pe_applies = not pol_methods or "P/E" in pol_methods     # the issuer policy excludes earnings multiples for some classes

    def _pe(den, mid, label, basis):
        ok = pe_applies and den is not None and bool(den.value) and den.value > 0 and _compat0(pm, den, "ratio")[0]
        v = (pm.value / den.value) if ok else None
        why = (f"price ÷ {label}" if pe_applies else "not applicable: this issuer's policy excludes earnings multiples")
        return metric(mid, "PE", v, den.period_type if den is not None else NTM, accounting_basis=basis, unit="x",
                      derivation=why, quality_status=DERIVED if v else UNAVAILABLE)
    s.pricing.pe_forward = _pe(eps_f, "pe_forward", "forecast EPS (next 12 months)", ADJUSTED)
    s.pricing.pe_trailing = _pe(eps_a, "pe_trailing", "trailing adjusted EPS", ADJUSTED)
    s.pricing.pe_trailing_gaap = _pe(eps_g, "pe_trailing_gaap", "trailing reported EPS", s.historical.eps_gaap_ttm.accounting_basis)
    g = s.value("forecast.eps_growth")
    peg = (s.value("pricing.pe_forward") / (g * 100.0)) if (s.value("pricing.pe_forward") and g and g > 0.02) else None
    s.pricing.peg_forward = metric("peg_forward", "PEG", peg, NTM, accounting_basis=ADJUSTED, unit="ratio",
                                   derivation="forward P/E ÷ forecast EPS growth (in percent)",
                                   quality_status=DERIVED if peg else UNAVAILABLE)
    # price-to-value relationships: computed once, here, through the compatibility gate. The
    # renderer only formats them; a direction word ("above"/"below") comes from the sign.
    from state import compatible as _compat

    def _gap(ref, mid, what):
        ok, _why = _compat(pm, ref, "ratio") if ref is not None else (False, [])
        v = (pm.value / ref.value - 1.0) if (ok and ref.value) else None
        return metric(mid, "PRICE_GAP", v, SPOT, accounting_basis=MARKET, unit="ratio",
                      derivation=f"price ÷ {what} − 1", quality_status=DERIVED if v is not None else UNAVAILABLE)
    s.pricing.price_vs_composite = _gap(s.pricing.market_composite, "price_vs_composite", "market composite")
    s.pricing.target_vs_price = metric("target_vs_price", "TARGET_GAP",
                                       (s.consensus.target_price.value / pm.value - 1.0)
                                       if (s.consensus.target_price.value and pm.value) else None, SPOT,
                                       accounting_basis=MARKET, unit="ratio", derivation="mean analyst target ÷ price − 1")
    s.valuation.price_vs_intrinsic = _gap(s.valuation.intrinsic_value, "price_vs_intrinsic", "intrinsic value")
    s.valuation.price_vs_mechanical = _gap(s.valuation.mechanical_value, "price_vs_mechanical", "mechanical value")
    # engine vs consensus only on the same twelve months (time-weighted FY1/FY2) and a compatible definition
    _fe, _ce = s.forecast.eps, s.consensus.get("eps_ntm")
    _ok, _why = _compat(_fe, _ce, "level") if _ce is not None else (False, ["no consensus for the forecast period"])
    s.forecast.eps_vs_consensus = metric("eps_vs_consensus", "EPS_GAP", (_fe.value / _ce.value - 1.0) if (_ok and _ce.value) else None,
                                         NTM, accounting_basis=ADJUSTED, unit="ratio",
                                         quality_status=DERIVED if (_ok and _ce.value) else UNAVAILABLE,
                                         derivation=("forecast adjusted EPS ÷ time-weighted consensus EPS for the same twelve months − 1; "
                                                     + (_defs.reconciliation_evidence(_fe, _ce) or "one adjustment definition"))
                                         if _ok else "not comparable: " + "; ".join(_why))
    pc = d.get("peer_comps") or {}

    PRICE_MULTIPLES = ("pe", "ev_ebitda", "ps", "fcf_yield", "div_yield")

    def _implausible_scale(r):
        # the panel keeps foreign filers in their reporting currency while market cap is in USD, so every
        # price-based multiple of such a row is off by the exchange rate; two or more impossible values
        # identify the row (withholding only — nothing is corrected or re-derived)
        hits = [(_f(r.get("pe")) is not None and 0 < _f(r.get("pe")) < 2),
                (_f(r.get("ev_ebitda")) is not None and 0 < _f(r.get("ev_ebitda")) < 1.5),
                (_f(r.get("ps")) is not None and _f(r.get("ps")) < 0.3 and (_f(r.get("op_margin")) or 0) > 0.10),
                (_f(r.get("fcf_yield")) is not None and _f(r.get("fcf_yield")) > 0.5),
                (_f(r.get("div_yield")) is not None and _f(r.get("div_yield")) > 0.2)]
        return sum(hits) >= 2

    def _meaningful(row):
        r = dict(row or {})
        if r.get("pe_unreconciled") or _implausible_scale(r):
            for k in PRICE_MULTIPLES:
                r[k] = None
            r["multiples_withheld"] = "price-based multiples on an inconsistent currency or scale basis"
        # a trailing P/E on negative earnings is not a multiple; it is published as unavailable
        if _f(r.get("pe")) is not None and _f(r.get("pe")) <= 0:
            r["pe"] = None
        return r
    _sep = (s.historical.get("perimeter") or {}).get("event")
    _subj = _meaningful(pc.get("subject")) if (pc.get("subject") and not _sep) else None
    # a derived statistic inherits the eligibility of every figure it is computed from: a percentile of a withheld
    # subject multiple is withheld with it (RelativeValuationClaim)
    _pct = {k: v for k, v in (pc.get("percentiles") or {}).items() if _subj and _subj.get(k) is not None}
    _why_withheld = ("its trailing figures include the separated business" if _sep else
                     (None if _subj else "the subject is not in the peer panel"))
    s.pricing.peers = {"subject": _subj, "subject_withheld": _why_withheld if not _subj else None,
                       "peers": [_meaningful(p) for p in (pc.get("peers") or [])], "percentiles": _pct,
                       "percentiles_withheld": (sorted(set(pc.get("percentiles") or {}) - set(_pct)) or None),
                       "as_of": pc.get("as_of"), "basis": pc.get("basis"), "sector": pc.get("sector"),
                       "source": "Sharadar panel (trailing multiples)"}
    _meta = {"pe": ("P/E", "TRAILING", "GAAP"), "ev_ebitda": ("EV/EBITDA", "TRAILING", "GAAP"), "ps": ("P/S", "TRAILING", "GAAP"),
             "fcf_yield": ("FCF yield", "TRAILING", "GAAP"), "op_margin": ("operating margin", "TRAILING", "GAAP"),
             "rev_growth": ("revenue growth", "TRAILING", "GAAP")}
    s.pricing.relative = {
        k: {"metric": lab, "subject_value": (_subj or {}).get(k), "peer_values": [p.get(k) for p in (pc.get("peers") or [])
                                                                                   if p.get(k) is not None],
            "percentile": _pct.get(k), "period_basis": per_b, "accounting_basis": acc_b,
            "perimeter_basis": ("pre-separation" if _sep else "consolidated"), "peer_perimeter_basis": "as reported",
            "comparison_allowed": _pct.get(k) is not None and not _sep,
            "reason": (None if (_pct.get(k) is not None and not _sep) else (_why_withheld or "no subject value"))}
        for k, (lab, per_b, acc_b) in _meta.items()}

    # quant: reliability decides what exists outside diagnostics
    s.quant.price_model = {"reliability": dict(quant_rel or {}),
                           "diagnostics_only": {"expected_return": _f((ml or {}).get("expected_return")),
                                                "direction_accuracy": _f((ml or {}).get("backtest_dir_accuracy")),
                                                "rank_ic": _f((ml or {}).get("backtest_rank_ic")),
                                                "mae": _f((ml or {}).get("backtest_mae")),
                                                "n_oos_eff": _f((ml or {}).get("n_oos_effective"))}}
    _pfig = (dict(panel_row, rank_share=(_f(panel_row.get("xs_rank_pct")) or 0.0) / 100.0)
             if (panel_row and (panel_rel or {}).get("persona_visibility")) else None)
    s.quant.panel_rank = {"reliability": dict(panel_rel or {}),
                          "figures": _pfig,
                          "diagnostics_only": dict(panel_row) if panel_row else None}

    # macro, technical, sentiment
    mr = macro or {}
    s.macro.view = {"return": _f(mr.get("expected_return")), "confidence": _f(mr.get("confidence"))}
    s.macro.signals = {k: v for k, v in (mr.get("signals") or {}).items() if isinstance(v, (int, float, str))}
    tr = tech or {}
    s.technical.view = {"return": _f(tr.get("expected_return")), "confidence": _f(tr.get("confidence"))}
    s.technical.signals = {k: v for k, v in (tr.get("signals") or {}).items() if isinstance(v, (int, float, str))}
    fd = fund or {}
    s.technical.scorecard = fd.get("scorecard")
    rc = d.get("risk_card") or {}
    s.technical.risk = {k: rc.get(k) for k in ("vol_1y", "max_drawdown_1y", "dist_52w_high", "corr_spy_1y")}
    sc = sent or {}
    comp_s = sc.get("components") or {}
    s.sentiment.news = {"score": _f(comp_s.get("news_sentiment")), "n_articles": comp_s.get("news_n_articles")}

    # insiders, leadership, corporate actions
    ic = d.get("insider_canon") or {}
    import insider as _ins0
    _ican = {k: ic.get(k) for k in ("characterization", "discretionary_activity_signal", "governance_signal",
                                    "plan_only", "plan_sale_value", "open_market_sale_value", "purchase_value",
                                    "window_months", "n_transactions", "note")}
    _ican["motive_inference_allowed"] = False
    _mc = (s.value("market.price") or 0) * (s.value("market.shares_outstanding") or 0)
    _ican["materiality"] = _ins0.materiality(_ican, _mc or None) if ic else None
    s.insiders.canon = _ican
    s.insiders.factor = {k: (ic.get("factor") or {}).get(k) for k in ("score", "basis")}
    # 13F ownership: positions, not intent, reported ~45 days after quarter end. A snapshot older than
    # two filing cycles is not current ownership and is withheld (kept in QA).
    o13 = d.get("ownership_13f") or {}
    age = None
    try:
        age = (_dt.date.today() - _dt.date.fromisoformat(str(o13.get("as_of"))[:10])).days if o13.get("as_of") else None
    except ValueError:
        age = None
    fresh = bool(o13) and age is not None and age <= 200
    names = [filer_display_name(x) for x in str(o13.get("guru_names") or "").split(",") if x.strip()]
    # the name list is a sample of the tracked holders (the snapshot keeps at most a few names): "and N others" counts
    # against the tracked-holder COUNT, never against the length of the sample
    shown = names[:6]
    n_tracked = int(round(o13["n_gurus"])) if o13.get("n_gurus") is not None else len(names)
    others = max(n_tracked - len(shown), 0)
    s.ownership.f13 = ({**{k: o13.get(k) for k in ("as_of", "n_holders", "n_holders_prev", "n_gurus", "guru_new_buys",
                                                   "guru_adds", "guru_trims", "guru_exits", "source")},
                        "guru_names_shown": shown, "guru_names_others": others,
                        "guru_names_short": (", ".join(shown) + (f" and {others} other{'s' if others != 1 else ''}" if others else ""))
                        if shown else None,
                        "n_holders_change": ((o13.get("n_holders") - o13.get("n_holders_prev"))
                                             if (o13.get("n_holders") is not None and o13.get("n_holders_prev") is not None) else None)}
                       if fresh else None)
    # OwnershipEvidence permissions: a 13F reports positions ~45 days after quarter end; it never records intent
    s.ownership.permissions = {"state_positions": True, "state_changes": True, "infer_intent": False,
                               "infer_motive": False, "use_as_return_signal": False}
    s.ownership.status = ("CURRENT" if fresh else ("STALE" if o13 else "UNAVAILABLE"))
    s.ownership.age_days = age
    s.ownership.diagnostics_only = dict(o13) if (o13 and not fresh) else None
    lc = d.get("leadership_canon") or {}
    s.leadership.roles = {r: {k: (lc.get(r) or {}).get(k) for k in ("name", "title", "source", "status", "filed")}
                          for r in ("CEO", "CFO", "COO") if isinstance(lc.get(r), dict) and (lc.get(r) or {}).get("name")}
    s.leadership.status = dict(d.get("leadership_status") or {})
    s.leadership.regime = lc.get("_regime")
    s.corporate_actions.states = [dict(x) for x in (d.get("corp_states") or []) if isinstance(x, dict)]
    s.corporate_actions.restatement = d.get("restatement")
    s.corporate_actions.pending = d.get("pending_deals") or []
    # the canonical CorporateActionState: one record per action id, status from filings only (corporate_actions.py)
    import corporate_actions as _ca
    _ev0 = dict((s.historical.get("perimeter") or {}).get("event") or {})
    _rsd = d.get("restatement") or {}
    if _ev0 and _rsd.get("business") == _ev0.get("business"):
        _ev0.update({k: _rsd.get(k) for k in ("edgar", "announced") if _rsd.get(k)})
    s.corporate_actions.state = _ca.build(s.identity.ticker, _ev0 or None, d.get("edgar_actions"), d.get("corp_actions"),
                                          d.get("segments_canonical"))
    # BusinessDecompositionState: the businesses a story may value, from the filed segment state and the canonical
    # corporate actions only (business.py). Rebuilt here from the state's own inputs, never taken from a storyteller.
    import business as _bz
    s.valuation.business_decomposition = _bz.build(
        s.identity.ticker, d.get("segments_canonical"), s.corporate_actions.state, d.get("story_base"),
        (d.get("perimeter_adjustment") or {}).get("segment_ids"))

    # qualitative context for the personas (text evidence, never numbers of record)
    s.context.news = [{"title": n.get("title"), "source": n.get("source"), "sentiment": n.get("overall_sentiment_label")}
                      for n in (d.get("news") or [])[:8] if isinstance(n, dict)]
    s.context.transcript_excerpt = (d.get("transcript_excerpt") or "")[:4000]
    s.context.edgar_actions = [dict(x) for x in (d.get("edgar_actions") or [])[:5] if isinstance(x, dict)]
    _cn_ctx = d.get("segments_canonical") or {}
    s.context.segments = ([{"segment_id": g.get("segment_id"), "name": g.get("name"), "revenue": g.get("revenue"),
                            "operating_income": g.get("operating_income"), "revenue_share": g.get("revenue_share")}
                           for g in _cn_ctx.get("segments") or []] if _cn_ctx.get("status") == "RECONCILED" else [])
    s.context.earnings_date = ((d.get("earnings_calendar") or [{}])[0] or {}).get("reportDate") if isinstance(d.get("earnings_calendar"), list) else None
    from state import stamp_perimeter as _stamp
    _stamp(s, (s.historical.get("perimeter") or {}).get("action_id"), ("forecast", "valuation", "pricing"))
    # segment counts per perimeter: the filed count describes the business AS FILED (before a separation); the
    # continuing count removes the separated segment(s). A count is never cited without its perimeter.
    _can = (s.segments.get("canonical") or {})
    _nseg = len(_can.get("segments") or []) if _can.get("status") == "RECONCILED" or _can.get("segments") else None
    _sep = list((s.valuation.get("perimeter_adjustment") or {}).get("segment_ids") or [])
    _hper = (s.historical.get("perimeter") or {})
    _paid = _hper.get("action_id")
    s.valuation.segment_count_filed = metric(
        "segment_count_filed", "SEGMENT_COUNT", _nseg, FY, unit="count", currency=None, quantity_type="LEVEL",
        perimeter=(_hper.get("historical") or CONSOLIDATED) if _paid else CONSOLIDATED, corporate_action_id=_paid,
        period=_can.get("period_end"), source=_can.get("source") or "filed segment note", quality_status=VALIDATED if _nseg else UNAVAILABLE)
    s.valuation.segment_count_continuing = metric(
        "segment_count_continuing", "SEGMENT_COUNT", (_nseg - len(_sep)) if (_nseg and _paid and _sep) else None, FY,
        unit="count", currency=None, quantity_type="LEVEL", perimeter=(_hper.get("forward") or CONSOLIDATED),
        corporate_action_id=_paid, period=_can.get("period_end"),
        derivation="filed segments less the separated segment(s)", quality_status=DERIVED if (_nseg and _paid and _sep) else UNAVAILABLE)
    # typed relations between typed quantities, and the scope of every model-derived claim
    import evidence as _evd
    import relations as _rel
    s.relations.state = _rel.build(s)
    s.evidence.state = _evd.build(s)
    return state


# ═══════════════════════════════════════════════════════════════════════════════════════
# SCENARIOS / BLEND / RECOMMENDATION (after the panel's structured judgement)
# ═══════════════════════════════════════════════════════════════════════════════════════
def build_scenarios(state, debate):
    """Structured panel judgement → ScenarioState. Prose is NOT stored here."""
    s = state
    db = debate or {}
    seats = db.get("seats") or {}
    rows = {}
    for key in ("bear", "neutral", "bull"):
        e = seats.get(key) or {}
        ret = _f(e.get("return"))
        # one rating table for every label in the document: a seat's label is the band of its return
        rows[key] = {"return": ret, "label": rating_for(round(ret, 3)) if ret is not None else None, "model": e.get("model"),
                     "confidence": _f(e.get("confidence"))}
    pb, pn, pu = _f(db.get("p_bear")), _f(db.get("p_neutral")), _f(db.get("p_bull"))
    for key, p in (("bear", pb), ("neutral", pn), ("bull", pu)):
        rows[key]["probability"] = p
    s.scenarios.bear = rows["bear"]
    s.scenarios.neutral = rows["neutral"]
    s.scenarios.bull = rows["bull"]
    complete = all(rows[k]["return"] is not None and rows[k]["probability"] is not None for k in rows)
    pm_ret = (sum(rows[k]["return"] * rows[k]["probability"] for k in rows)) if complete else None
    s.scenarios.complete = complete
    s.scenarios.pm_return = metric("pm_return", "PM_SCENARIO_RETURN", pm_ret, NTM, accounting_basis=MODEL, unit="ratio",
                                   derivation="Σ scenario probability × scenario price return",
                                   quality_status=MODEL_ESTIMATE if complete else UNAVAILABLE)
    s.scenarios.pm_label = rating_for(round(pm_ret, 3)) if pm_ret is not None else None
    s.scenarios.pm_confidence = _f(db.get("pm_confidence"))
    s.scenarios.panel_complete = bool(db.get("panel_complete")) and complete
    s.scenarios.missing_roles = list(db.get("missing_roles") or [])
    s.scenarios.probability_audit = db.get("scenario_probability_audit")
    s.scenarios.evidence = db.get("evidence_lineage")
    # {seat: {accepted, rejected}}: the evidence state the portfolio manager was given when it set the probabilities
    s.scenarios.pm_evidence_state = db.get("pm_evidence_state")
    typed = [(t, _typed_trigger(t, s)) for t in (db.get("thesis_triggers") or [])]
    # a "what would change the view" condition must not already hold in the current state: such a trigger is omitted
    s.scenarios.triggers = [x for _t, x in typed if x and not x.get("currently_satisfied")]
    s.scenarios.triggers_dropped = [{"metric_id": t.get("metric_id"), "threshold": t.get("threshold"), "operator": t.get("condition_operator"),
                                     "source_claim": t.get("source_claim"),
                                     "reason": ("already satisfied by the current state: " + ", ".join(
                                         f"{c} = {v:g}" for c, v in (x.get("current_values") or []) if trigger_holds(x, v))
                                                if x else (trigger_source_refusal(s, t.get("source_claim"))
                                                           or "untyped or unit/magnitude inconsistent with its source"))}
                                    for t, x in typed if not x or x.get("currently_satisfied")]
    return state


_OPS = {">": "above", ">=": "above", "<": "below", "<=": "below"}


_UNIT_STYPE = {"BASIS_POINTS": "BPS", "PERCENTAGE_POINTS": "PP", "PERCENT": "PCT", "DECIMAL_FRACTION": "PCT",
               "MULTIPLE": "MULTIPLE", "USD_PER_SHARE": "CURRENCY", "USD": "CURRENCY_BIG", "USD_MILLION": "CURRENCY_BIG",
               "USD_BILLION": "CURRENCY_BIG"}
_STYPE_UNIT = {"PCT": "PERCENT", "PCT_SIGNED": "PERCENT", "MULTIPLE": "MULTIPLE", "CURRENCY": "USD_PER_SHARE",
               "CURRENCY_BIG": "USD", "PP": "PERCENTAGE_POINTS", "BPS": "BASIS_POINTS"}


def _written_unit(raw):
    """(number, unit written or None) for a threshold string: '35 bps', '6%', '17x', '$3.2B', '35'."""
    import re as _re
    m = _re.fullmatch(r"(-|−)?\s*(\$)?\s*(\d+(?:\.\d+)?)\s*(%|bps|bp|basis points?|pp|pts|percentage points?|x|B|bn|M)?",
                      raw.replace(",", "").strip(), _re.I)
    if not m:
        return None, None
    v = float(m.group(3)) * (-1 if m.group(1) else 1)
    u = (m.group(4) or "").lower()
    if m.group(2):
        return (v * 1e9, "USD") if u in ("b", "bn") else ((v * 1e6, "USD") if u == "m" else (v, "USD_PER_SHARE"))
    unit = {"%": "PERCENT", "bps": "BASIS_POINTS", "bp": "BASIS_POINTS", "basis point": "BASIS_POINTS",
            "basis points": "BASIS_POINTS", "pp": "PERCENTAGE_POINTS", "pts": "PERCENTAGE_POINTS",
            "percentage point": "PERCENTAGE_POINTS", "percentage points": "PERCENTAGE_POINTS", "x": "MULTIPLE"}.get(u)
    return v, unit


def _source_unit(state, cid):
    """(unit, quantity_type, guidance_id) of the claim a trigger is measured against."""
    import catalog as _cat
    if not cid or state is None:
        return None, None, None
    if cid.startswith("GUIDANCE_"):
        g = state.get("guidance.company") or {}
        try:
            m = (g.get("metrics") or [])[int(cid.split("_")[1]) - 1]
        except (ValueError, IndexError):
            return None, None, None
        return m.get("unit"), m.get("quantity_type"), m.get("metric_id")
    d = _cat.BY_ID.get(cid)
    if d is None:
        return None, None, None
    m = state.get(d.path)
    qt = getattr(m, "quantity_type", None)
    return (getattr(m, "display_unit", None) or _STYPE_UNIT.get(d.stype)), qt, None


def _source_range(state, cid):
    """(low, high) canonical value of a trigger's source claim, or (None, None)."""
    import catalog as _cat
    if not cid or state is None:
        return None, None
    if cid.startswith("GUIDANCE_"):
        try:
            m = ((state.get("guidance.company") or {}).get("metrics") or [])[int(cid.split("_")[1]) - 1]
        except (ValueError, IndexError):
            return None, None
        return m.get("low"), m.get("high")
    d = _cat.BY_ID.get(cid)
    if d is None:
        return None, None
    v = state.value(d.path) if hasattr(state, "value") else None
    return (v, None) if isinstance(v, (int, float)) else (None, None)


def trigger_holds(tr, v):
    """Does value v already meet the trigger's condition?"""
    th = tr.get("threshold")
    if v is None or th is None:
        return False
    return float(v) >= float(th) if tr.get("side") == "above" else float(v) <= float(th)


def trigger_current_values(state, source_claim):
    """[(claim id, value)]: every current value of the metric a trigger measures — the source claim and every other
    claim of the same concept, period class and perimeter (e.g. the engine's and the consensus' NTM EPS). Guidance is a
    target, not a current value of the realised metric, and is excluded."""
    import catalog as _cat
    if not source_claim or state is None or str(source_claim).startswith("GUIDANCE_"):
        return []
    src = _cat._claim_metric(state, source_claim)
    if src is None or src.value is None:
        return []
    out = []
    for cid, d in _cat.BY_ID.items():
        if d.stype in (_cat.REL, _cat.PHRASE, _cat.TEXT) or d.deferred:
            continue
        m = _cat._claim_metric(state, cid)
        if m is None or m.value is None:
            continue
        if (m.economic_concept, m.period_class, m.perimeter) == (src.economic_concept, src.period_class, src.perimeter) \
                and (m.per_share == src.per_share):
            out.append((cid, float(m.value)))
    return out


_PRICE_MULTIPLES = frozenset({"PE", "PEG", "PS", "EV_EBITDA", "EV_SALES", "P_FCF", "PRICE_TO_BOOK", "PB"})


def trigger_source_refusal(state, cid):
    """Why a claim cannot be a trigger's source, or None: a figure of another business (a trailing figure that still
    includes a separated one), or a price-derived multiple — it moves with the price itself, so "P/E falls to 17x" can
    read as cheaper shares or as the bear case arriving (all three reviewers flagged run 45cf885f88330ebb's)."""
    if trigger_perimeter_mismatch(state, cid):
        return "its source is a trailing figure that includes a separated business"
    if state is None or not cid:
        return None
    import catalog as _cat
    try:
        m = _cat._claim_metric(state, cid)
    except Exception:  # noqa: BLE001
        return None
    if m is not None and str(m.economic_concept).upper() in _PRICE_MULTIPLES:
        return "its source is a price-derived multiple, which moves with the price itself"
    return None


def trigger_perimeter_mismatch(state, cid):
    """True when a trigger's source claim describes a different business from the one the forecast describes: a
    trailing figure that still includes a separated business moves mechanically as that business rolls out of the
    window, so a threshold on it does not measure the thesis (run 2a5c13806e3f0b7f: a trigger on the pre-separation
    trailing margin, 46.3%, while the continuing company runs at 49.4%)."""
    if state is None or not cid:
        return False
    import catalog as _cat
    try:
        m = _cat._claim_metric(state, cid)
    except Exception:  # noqa: BLE001
        return False
    return bool(m is not None and m.perimeter == "pre-separation"
                and ((state.historical.get("perimeter") or {}).get("event") or {}).get("business"))


_LEGAL_SUFFIX = {"LLC": "LLC", "LLP": "LLP", "LP": "LP", "PLC": "PLC", "UK": "UK", "USA": "USA", "NA": "NA", "AG": "AG",
                 "SA": "SA", "NV": "NV", "BV": "BV", "AB": "AB", "ASA": "ASA", "SE": "SE", "LTD": "Ltd", "INC": "Inc",
                 "CORP": "Corp", "CO": "Co"}


def filer_display_name(name):
    """A 13F filer's name as a reader writes it: legal-form and country tokens in their standard case ("Brave Warrior
    Advisors Llc" → "… LLC", "Egerton Capital Uk Llp" → "… UK LLP"); every other word as the source gives it."""
    words = str(name or "").strip().split()
    return " ".join(_LEGAL_SUFFIX.get(w.strip(".,").upper(), w) if i and w.strip(".,").upper() in _LEGAL_SUFFIX else w
                    for i, w in enumerate(words))


def _typed_trigger(t, state=None):
    """A thesis trigger is a structured judgement: a metric, a comparison and a typed threshold. A trigger that names
    its source claim INHERITS that claim's unit and quantity type (35 against a bps value is 35 bps); a threshold whose
    written unit contradicts its source (35% against a bps value) is not publishable and stays in QA."""
    import quantities as _q
    op = str(t.get("condition_operator") or "").strip()
    raw = str(t.get("threshold") or "").strip()
    if op not in _OPS or not raw:
        return None
    num, written = _written_unit(raw)
    if num is None:
        return None
    src_unit, qt, gid = _source_unit(state, t.get("source_claim"))
    if t.get("source_claim") and src_unit is None:
        return None                                   # names a claim that does not exist
    if trigger_source_refusal(state, t.get("source_claim")):
        return None                                   # a source that cannot measure the thesis (see the reason)
    if src_unit:
        money = ("USD", "USD_MILLION", "USD_BILLION")
        same = (written is None or written == src_unit or {written, src_unit} <= {"PERCENT", "DECIMAL_FRACTION"}
                or (written in money and src_unit in money))
        if not same:
            return None                               # unit detached from its source: 35% against 35 bps
        unit = src_unit
        value = num if (written in ("USD", "USD_PER_SHARE") and src_unit in money + ("USD_PER_SHARE",)) \
            else _q.to_canonical(num, unit)
    else:
        if written is None:
            return None
        unit = written
        value = num if unit in ("USD", "USD_PER_SHARE") else _q.to_canonical(num, unit)
    if src_unit and not _q.magnitude_ok(value, *_source_range(state, t.get("source_claim")))[0]:
        return None                                   # 3.6 against 335–360 bps: the right unit, the wrong number
    cur = trigger_current_values(state, t.get("source_claim"))
    probe = {"side": _OPS[op], "threshold": value}
    sat = [c for c, v in cur if trigger_holds(probe, v)]
    # a guidance source is a RANGE (a target, not a realised value): encode where the threshold sits in it, never a scalar
    rng, rng_state = None, None
    if str(t.get("source_claim") or "").startswith("GUIDANCE_"):
        lo, hi = _source_range(state, t.get("source_claim"))
        if lo is not None:
            hi = lo if hi is None else hi
            rng = {"kind": "GUIDANCE_RANGE", "low": lo, "high": hi, "source_claim_id": t.get("source_claim")}
            ends = [trigger_holds(probe, lo), trigger_holds(probe, hi)]
            if all(ends):
                rng_state = "SATISFIED_ACROSS_RANGE"
            elif not any(ends):
                rng_state = "OUTSIDE_RANGE"
            elif abs(value - lo) < 1e-12 or abs(value - hi) < 1e-12:
                rng_state = "AT_RANGE_ENDPOINT"
            else:
                rng_state = "INSIDE_RANGE"
            if rng_state == "SATISFIED_ACROSS_RANGE":
                sat = sat or [str(t.get("source_claim"))]
    return {"metric_id": t.get("metric_id"), "side": _OPS[op], "threshold": value, "threshold_kind": _UNIT_STYPE.get(unit, "TEXT"),
            "current_value": (dict(cur).get(sat[0]) if (sat and cur) else (cur[0][1] if cur else None)), "current_values": cur,
            "current_range": rng, "range_position": rng_state,
            "currently_satisfied": bool(sat),
            "threshold_unit": unit, "quantity_type": qt, "source_claim_id": t.get("source_claim"), "source_guidance_id": gid,
            "scenario_destination": t.get("scenario_destination"), "polarity": t.get("polarity"), "status": t.get("status")}


RATING_BANDS = ((0.20, "STRONG BUY"), (0.10, "BUY"), (-0.05, "HOLD"), (-0.20, "SELL"))


def rating_for(ret):
    """The one rating table. Applied to the ROUNDED headline, so the label always matches the
    number the reader sees."""
    if ret is None:
        return None
    for lo, lab in RATING_BANDS:
        if ret >= lo - 1e-12:
            return lab
    return "STRONG SELL"


BLEND_KINDS = ("MARKET_PRICING", "INTRINSIC_VALUE", "MACRO", "PRICE_MODEL", "ANALYST_PANEL", "OTHER")
BLEND_KIND_LABEL = {"MARKET_PRICING": "Market pricing", "INTRINSIC_VALUE": "Intrinsic value", "MACRO": "Macro backdrop",
                    "PRICE_MODEL": "Machine-learning price model", "ANALYST_PANEL": "Analyst panel", "OTHER": "Other source"}


def build_blend(state, blend):
    s = state
    b = blend or {}
    audit = b.get("blend_audit") or {}
    rows, excluded = [], []
    for r in audit.get("rows") or []:
        row = {k: r.get(k) for k in ("name", "return", "raw_return", "cap_note", "confidence", "base_weight",
                                     "corr_penalty", "eff_weight", "share", "contribution")}
        # BlendSource: kind and provenance as declared by the source's creator; the label follows the kind
        row["kind"] = r.get("kind") if r.get("kind") in BLEND_KINDS else "OTHER"
        row["underlying_object_ids"] = list(r.get("underlying_object_ids") or [])
        row["label"] = BLEND_KIND_LABEL[row["kind"]] if row["kind"] != "OTHER" else f"Other source ({row.get('name')})"
        row["source_id"] = row.get("name")
        # a source with no weight contributes nothing and its view is not a figure of record: it is
        # listed by name only, so an excluded (e.g. unreliable) model's number never reaches a reader
        if not (_f(row.get("share")) or 0.0) > 0:
            excluded.append({"name": row["label"], "kind": row["kind"], "reason": row.get("cap_note") or "zero weight"})
            continue
        rows.append(row)
    s.blend.sources = rows
    s.blend.excluded_sources = excluded
    raw = _f(b.get("expected_return"))
    head = round(raw, 3) if raw is not None else None       # one precision: 0.1 percentage point
    s.blend.headline_raw = raw
    s.blend.headline_return = metric("headline_return", "HEADLINE_RETURN", head, NTM, accounting_basis=MODEL, unit="ratio",
                                     derivation="clip(Σ source return × normalised share); rounded to 0.1 pt before rating",
                                     quality_status=MODEL_ESTIMATE)
    s.blend.weighted_sum = _f(audit.get("weighted_sum"))
    s.blend.adjustments = [{"name": a.get("name"), "factor": _f(a.get("factor")), "note": a.get("note")}
                           for a in (audit.get("adjustments") or []) if isinstance(a, dict)]
    s.blend.formula = audit.get("formula")
    s.blend.return_kind = "price return (dividends excluded)"
    s.blend.agreement = _f(b.get("confidence"))
    s.recommendation.rating = rating_for(head)
    s.recommendation.bands = [{"from": lo, "label": lab} for lo, lab in RATING_BANDS] + [{"from": None, "label": "STRONG SELL"}]
    s.recommendation.basis = "fixed bands on the headline return"
    # uncertainty: the scenario range, never an unreliable model's error
    bear, bull = s.scenarios.get("bear") or {}, s.scenarios.get("bull") or {}
    lo, hi = bear.get("return"), bull.get("return")
    s.uncertainty.scenario_range = {"low": min(lo, hi) if (lo is not None and hi is not None) else None,
                                    "high": max(lo, hi) if (lo is not None and hi is not None) else None,
                                    "basis": "bear and bull scenario returns from the analyst panel"}
    return state