monetize-product 0.3.2

The product plugin trait for monetize: how a metered product reports Usage and receives an Entitlement. Types and one trait, serde only — no ledger, no vendor, no network.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
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
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
//! **The product as monetize serves it: its facts, and its estate over the pull channel.**
//!
//! monetize handles MANY products (gunnar.rs, holger.rs, ...) and knows none of them by name.
//! A product is two things here, and neither is a per-product adapter:
//!
//! * its **facts** ([`ProductFacts`]): its id, its meters, its words, its ticket and test
//!   buttons, sent by the product's own signed guest and read once by the host;
//! * its **estate** ([`pulled::Pulled`]): the newest report of the estate's main
//!   ([`pull::ProductReport`]) for every answer, and a signed order the main PULLS for every
//!   change. monetize never calls into an estate. [`Served`] holds the two side by side.
//!
//! ```text
//!   estate main ──report (pull channel)──▶ Pulled ──▶ monetize core
//!   estate main ◀──signed order, pulled─── Pulled ◀── monetize core
//! ```
//!
//! # Two directions, both facts
//!
//! * [`pulled::Pulled::read_usage`]: what a tenant is consuming, in the product's own units
//!   (gunnar: pack bytes, read-cache bytes, LFS bytes, tombstoned bytes, open-store RAM;
//!   holger: whatever holger meters), read from the main's report as [`Usage`], a flat bag of
//!   named meters, so core never has product-specific fields.
//! * [`pulled::Pulled::push_entitlement`]: the verdict, queued as a signed entitlement fact the
//!   main pulls and appends to its attestation log.
//!
//! # Growth
//!
//! A box grows through the v2 pull path only: a paid step is a signed grow go-ahead the box
//! pulls, and the box asks `Fleet.Ask{grow_set}` to size its own data disks (monetize-server
//! `estate_order::sold::grow_own`, journaled by `monetize_cloud::grow`). An order past the room
//! is queued with [`pulled::Pulled::queue_room`] (a signed `grow_room` order). There is no
//! appliance grow drive here.
//!
//! # monetize never blocks the product
//!
//! Counting is done **inside the product, always, open source**: a self-hoster wants the
//! numbers too. monetize only *reads* them. If monetize is down the product keeps serving on
//! its cached entitlement; the next report and the next pulled order catch up.

use std::collections::BTreeMap;

/// The registration seam: how a product gets into a server without the server
/// knowing its name. A build registers what it links; the server looks ids up.
pub mod factory;
pub use factory::{
    body_secret_placeholder, GuestSpec, HttpAuth, ProductBuild, ProductFactory,
    PULLED_NEVER_DIALLED,
};

/// **Reading a daily `*_today` counter out of a series of readings** — the fold
/// behind the console's SILENT and FRICTION lists. Pure; see the module doc for
/// why summing the readings is the wrong arithmetic and why it lives here.
pub mod flow;
pub mod guardian;
pub mod pull;
pub mod pulled;

/// A tenant, as the product names it. gunnar: the `Namespace` name (`team/sub`).
#[derive(
    Clone,
    PartialEq,
    Eq,
    Hash,
    PartialOrd,
    Ord,
    Debug,
    serde::Serialize,
    serde::Deserialize,
    Default,
)]
pub struct TenantId(pub String);

/// **Whether the product will actually POLICE a cap on this meter.**
///
/// CPU and RAM exist as quota even where gunnar cannot cap them, and that gap is
/// narrowed without redoing the engine.
///
/// An order may cap any declared meter. But a cap on a meter nothing checks is a
/// promise nobody keeps, and the tenant cannot tell the difference from the
/// invoice. So the product says, per meter, which kind of promise it is — and
/// the console shows it beside the number rather than letting every cap look
/// like a guarantee.
///
/// This is the smallest honest way to let CPU and RAM be sold: they ARE bought,
/// with real money, and they DO change what the box can do — they are simply not
/// something a git server polices per namespace. Saying so is better than either
/// refusing to sell them or pretending they are enforced.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Enforcement {
    /// **The product refuses when over.** A cap here is a wall the tenant hits.
    /// gunnar: `pack_bytes` (receive-pack refused before the bytes),
    /// `cache_bytes` (the explode budget).
    Enforced,
    /// **Counted and reported, not policed.** The number is true and monetize
    /// can bill on it or a human can act on it, but nothing refuses. A cap here
    /// is a threshold, not a wall.
    Measured,
    /// **Neither counted nor policed BY THE PRODUCT — it is iron monetize
    /// bought.** CPU and RAM: a bigger plan at the provider, real money, real
    /// effect, and no per-namespace check anywhere in the product. The
    /// provider's invoice is the enforcement.
    Provisioned,
}

impl Enforcement {
    /// One line for the console, beside the cap.
    pub fn describe(self) -> &'static str {
        match self {
            Enforcement::Enforced => "the product refuses when over",
            Enforcement::Measured => "counted and reported; nothing refuses",
            Enforcement::Provisioned => {
                "provisioned at the cloud provider; not policed by the product"
            }
        }
    }

    /// Is a cap on this meter a wall the tenant will actually hit?
    pub fn is_a_wall(self) -> bool {
        matches!(self, Enforcement::Enforced)
    }
}

/// **What a cap on this meter is MADE OF** — and therefore what UNIT the price
/// list prices it in and what iron, if any, the transaction buys for it.
///
/// Three of the four are capacity: bytes and cores, priced per GiB or core
/// per month, and (for disk) bought from a cloud. The fourth, [`Backing::Units`],
/// is a COUNT that is nevertheless SOLD — a property, a seat, a
/// mailbox — priced flat per unit per month and buying nothing. It exists
/// because a meter with no backing at all costs nothing to promise, and a
/// product whose whole price is a count (a per-property price) would
/// otherwise open a checkout for zero.
///
/// A plan that sells bytes it never buys is the one bug here that silently
/// converts money into nothing. Since the plan catalogue went (2026-09-05), an
/// ORDER names caps and nothing else, and this is the field that turns a cap
/// into a purchase: `monetize::order::resources_for` translates the DELTA on a
/// disk-backed meter into a `Resource::Disk`, and `monetize::PriceList` prices
/// each backing's delta by its own unit.
///
/// It lives on the meter and not on the order because only the PRODUCT knows
/// what its meter is made of — that `pack_bytes` is block storage and `pushes`
/// is a count of events that costs nothing to promise.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Backing {
    /// **Bytes of block storage.** A delta here becomes a `Resource::Disk` of
    /// `ceil(bytes / 2^30)` GiB, and is priced per GiB per month
    /// (`disk_gib_month` on the price list).
    DiskBytes,
    /// **CPU, in millicores (1000 = one core).** Priced per core per month
    /// (`cpu_core_month`). monetize does NOT translate millicores into a
    /// provider's plan name (`2xCPU-4GB` is a name, and a table of names goes
    /// stale every time a provider renames a size), so a delta here buys no
    /// cloud resource of its own: the ceiling is the product's own measurement
    /// (`Pulled::can_absorb`) and the box's declared limit
    /// (`RoomLeft::ceilings`).
    CpuMillicores,
    /// **RAM, in bytes.** Priced per GiB per month (`ram_gib_month`); the same
    /// ceiling rule as [`Backing::CpuMillicores`].
    RamBytes,
    /// **A counted, sold unit that is not iron** — a property register's
    /// `fastigheter`, a seat, a mailbox. Priced flat per unit per month (`unit_month`, the
    /// delta times the price, no rounding), and it buys NO cloud resource:
    /// `resources_for` ignores it exactly as it ignores CPU and RAM. The
    /// ceiling is the product's own answer (`Pulled::can_absorb`).
    ///
    /// Without this a product priced per count had no way to be priced at all:
    /// a meter with no backing "costs nothing to promise", which is right for
    /// `pushes` and wrong for a property that is the whole subscription.
    Units,
}

impl Backing {
    /// The price-list key this backing is priced under, and the unit it means:
    /// `disk_gib_month` | `cpu_core_month` | `ram_gib_month` | `unit_month`.
    pub fn unit(self) -> &'static str {
        match self {
            Backing::DiskBytes => "disk_gib_month",
            Backing::CpuMillicores => "cpu_core_month",
            Backing::RamBytes => "ram_gib_month",
            Backing::Units => "unit_month",
        }
    }

    /// Every backing, in the order the price list is written in.
    pub const ALL: [Backing; 4] = [
        Backing::DiskBytes,
        Backing::CpuMillicores,
        Backing::RamBytes,
        Backing::Units,
    ];

    /// The backing a price-list key names, if any.
    pub fn from_unit(unit: &str) -> Option<Backing> {
        Backing::ALL.into_iter().find(|b| b.unit() == unit)
    }

    /// **Price `delta` of this backing at `minor_per_unit_month`, for one
    /// month**, in minor units. Integer arithmetic, rounded UP to the unit for
    /// bytes (a customer who asks for one byte over a GiB is sold the next GiB,
    /// which is what the provider sells us), exact per millicore for CPU
    /// (`minor × millicores / 1000`), and flat for a counted unit (`delta ×
    /// minor` — five properties cost five times one).
    pub fn price_month(self, delta: u64, minor_per_unit_month: u64) -> u64 {
        const GIB: u64 = 1 << 30;
        match self {
            Backing::DiskBytes | Backing::RamBytes => {
                delta.div_ceil(GIB).saturating_mul(minor_per_unit_month)
            }
            Backing::CpuMillicores => {
                u64::try_from(u128::from(delta) * u128::from(minor_per_unit_month) / 1000)
                    .unwrap_or(u64::MAX)
            }
            Backing::Units => delta.saturating_mul(minor_per_unit_month),
        }
    }
}

/// **WHERE a meter's number is even meaningful: per tenant, or per product.**
///
/// Disk is the whole game, and it is per tenant. CPU and RAM stay in the model,
/// but for gunnar they are not interesting per user — they are interesting per
/// product: a tenant does not own cores, the box does.
///
/// Orthogonal to [`Enforcement`], and the pair is what makes a fleet total
/// honest. `Enforcement` says whether a cap is a WALL. Scope says whether adding
/// one tenant's figure to another's produces anything at all:
///
/// * `pack_bytes` is per tenant, so nine tenants' pack bytes sum to the fleet's
///   pack bytes;
/// * `cpu_millicores` is per product, so nine tenants' CPU does not sum to
///   anything — nobody measured a tenant's cores, and adding nine numbers that
///   were never measured is arithmetic on nothing.
///
/// **It is the PRODUCT's call, not a global rule.** gunnar meters disk per tenant
/// and CPU per product; another product may legitimately meter CPU per user, or
/// disk only in total. [`ProductFacts::meters`] is already that seam, and nothing
/// outside a product decides which of its meters are per-tenant. `monetize::fleet`
/// sums only [`Scope::Tenant`], whatever a product declares.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Scope {
    /// **Measured, or sold, per TENANT.** A fleet figure is the sum over tenants.
    /// The default, because it is what a meter usually is.
    Tenant,
    /// **Measured, or bought, for the WHOLE PRODUCT.** It appears ONCE, on the
    /// product screen, and never in a per-tenant sum. A cap on it may still be
    /// sold — CPU and RAM are bought with real money — it simply is not a
    /// quantity any one tenant holds.
    Product,
}

impl Scope {
    /// May a figure for this meter be summed across tenants? Only [`Scope::Tenant`].
    pub fn sums_across_tenants(self) -> bool {
        matches!(self, Scope::Tenant)
    }

    /// `tenant` | `product` — the wire and JSON spelling, one writer.
    pub fn name(self) -> &'static str {
        match self {
            Scope::Tenant => "tenant",
            Scope::Product => "product",
        }
    }
}

/// **One meter a product declares**: its name, what it means, and whether a cap
/// on it is policed. An order may only cap a declared meter, and the console
/// shows [`Enforcement`] beside the number.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Meter {
    /// The wire name, and the key in [`EntitlementFact::caps`] and [`Usage::meters`].
    pub name: &'static str,
    /// One line, the plugin's own words.
    pub meaning: &'static str,
    pub enforcement: Enforcement,
    /// **Does [`pulled::Pulled::read_usage`] carry a reading for this meter?**
    ///
    /// A meter exists to do one or both of two jobs: carry a usage READING, and
    /// accept a CAP. Most do both. Two kinds do not, and conflating them was
    /// caught by a test rather than by thinking:
    ///
    /// * `cpu_millicores`, `ram_bytes` — the product has no idea what was
    ///   bought; monetize does, from the order. Reporting `0` would read as "you
    ///   have none", which is worse than saying nothing.
    /// * `concurrent_transfers` — a cap the engine enforces instantly, with no
    ///   stored reading to bill from. The limit is real; the gauge does not exist.
    ///
    /// So a plugin's `read_usage` must report EXACTLY the meters with this set,
    /// which is an equality a test can hold in both directions: an undeclared
    /// reading is a leak, and a declared reading that stopped arriving is a
    /// silently emptied bill.
    pub reports_usage: bool,
    /// **What a cap on this meter is made of**, or `None` when a cap here costs
    /// nothing to grant (a count of pushes, a number of repositories the
    /// existing box already holds). `Some` is what makes an order on this meter
    /// a PURCHASE: the delta is priced by the backing's unit and, for disk,
    /// bought as iron. See [`Backing`].
    pub backing: Option<Backing>,
    /// **Whether this meter's number is per tenant or per product.** See
    /// [`Scope`]; it is what keeps a fleet total from adding up figures nobody
    /// measured per tenant.
    pub scope: Scope,
}

impl Meter {
    /// A meter that both reports a reading and may be capped — the common case.
    pub const fn new(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
        Meter {
            name,
            meaning,
            enforcement,
            reports_usage: true,
            backing: None,
            scope: Scope::Tenant,
        }
    }

    /// A meter that may be CAPPED but carries no reading. See
    /// [`Meter::reports_usage`] for the two kinds and why `0` is not a truthful
    /// substitute.
    pub const fn cap_only(
        name: &'static str,
        meaning: &'static str,
        enforcement: Enforcement,
    ) -> Meter {
        Meter {
            name,
            meaning,
            enforcement,
            reports_usage: false,
            backing: None,
            scope: Scope::Tenant,
        }
    }

    /// The same meter, declaring what a cap on it is MADE OF — and so what an
    /// order on it buys and what the price list prices it by. A
    /// [`Enforcement::Provisioned`] meter without one is a cap monetize can
    /// neither price nor buy; gunnar's own test holds its list to that.
    pub const fn with_backing(self, backing: Backing) -> Meter {
        Meter {
            backing: Some(backing),
            ..self
        }
    }

    /// The same meter, measured for the WHOLE PRODUCT rather than per tenant —
    /// so a fleet total leaves it out instead of adding up figures nobody took.
    /// See [`Scope`].
    pub const fn per_product(self) -> Meter {
        Meter {
            scope: Scope::Product,
            ..self
        }
    }
}

/// **What a product says it could still serve, or why it could not say.**
///
/// The other half of the oversell number: `sum(what has been sold) − servable`.
/// Without it monetize could sell ten tenants 10 GiB each on a box with 55 GiB
/// servable and nothing would object until the sixth push.
///
/// The two arms are the distinction `docs/v2/06-data-model.md` §6.1 rule 3 turns on and the same
/// one [`pulled::Pulled::can_absorb`] already makes: **an unanswered capacity question
/// is a measurement that did not happen, not a full disk and not an empty one.**
/// The live gunnar.rs appliance predates gunnar's `Admin.Capacity` RPC and
/// answers [`Servable::Unmeasured`] today; a fleet total that turned that into a
/// zero would report every deployment as catastrophically oversold, and one that
/// turned it into infinity would report every deployment as fine. Neither is a
/// measurement.
/// **Can this product's store grow while it runs?** Carried on
/// [`Servable::Measured`] beside the bytes, because the two answer different
/// questions and an operator acts differently on each: a full box that can grow
/// wants a disk; a full box that is SEALED wants its set enlarged by an operator
/// maintenance operation (the volumes it already has, grown in place) or a fresh
/// install onto a larger one, and a disk bought for it is billed and invisible.
///
/// Each arm carries the PRODUCT's own remediation sentence (gunnar's
/// `remediation()`), so the console renders what the product said rather than a
/// paraphrase of it — `docs/v2/07-console.md` screen 2's `⚠ cannot grow` badge holds exactly
/// that paragraph.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Growth {
    /// Cannot gain capacity while it runs, and nothing on the machine can give
    /// it any. `GROWTH_SEALED` on gunnar's wire.
    Sealed(String),
    /// The product will not grow the set itself, but an operator can, out of
    /// band. `GROWTH_OPERATOR_ONLY`.
    OperatorOnly(String),
    /// The product could not tell. NOT a promise that it can grow.
    Unknown(String),
    /// **Grows at its next restart** once every member volume has been
    /// enlarged to one equal size (gunnar's `GROWTH_AT_RESTART`, 2026-09-14).
    /// Still no hot-plug: a disk ATTACHED to it is as invisible as under
    /// `Sealed`; what helps is the outside actor enlarging the members
    /// (`monetize_cloud::grow`).
    AtRestart(String),
    /// **Grows without a restart**: the product drains, unmounts and grows into
    /// enlarged members while its control plane stays up (gunnar's
    /// `GROWTH_AT_RUNTIME`).
    AtRuntime(String),
}

impl Growth {
    /// The wire word: `sealed` | `operator_only` | `unknown` | `at_restart` |
    /// `at_runtime`.
    pub fn name(&self) -> &'static str {
        match self {
            Growth::Sealed(_) => "sealed",
            Growth::OperatorOnly(_) => "operator_only",
            Growth::Unknown(_) => "unknown",
            Growth::AtRestart(_) => "at_restart",
            Growth::AtRuntime(_) => "at_runtime",
        }
    }

    /// The product's own sentence about what to do.
    pub fn detail(&self) -> &str {
        match self {
            Growth::Sealed(s)
            | Growth::OperatorOnly(s)
            | Growth::Unknown(s)
            | Growth::AtRestart(s)
            | Growth::AtRuntime(s) => s,
        }
    }
}

#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Servable {
    /// The product measured its own store.
    Measured {
        /// **Bytes that may still be handed to tenants**, beyond what they already
        /// hold, after whatever reserve the product keeps for itself. gunnar's
        /// `servable_bytes` (`free − disk floor`) — never its `free_bytes`, which
        /// includes a reserve monetize must not sell.
        servable_bytes: u64,
        /// The whole store, reserve and all. `used + servable_bytes` is what the
        /// fleet could grow to; `total_bytes` is larger than that by the reserve
        /// and by anything on the filesystem that is not tenant data. 0 when the
        /// product measures what is left but not what there is in total.
        total_bytes: u64,
        /// **Whether the store can EVER hold more than `total_bytes`.** A
        /// measurement of the box, independent of the bytes: gunnar's
        /// `Admin.Capacity` answers it as `growth`, and it is the field that
        /// stops "buy another disk" being the reflex answer to a full fleet.
        growth: Growth,
        /// **The box's MEASURED ceilings on product-scope meters**, by the
        /// product's own meter name: gunnar's `cpu_millicores_total` and
        /// `ram_bytes_total` (Admin.Capacity, since gunnar 744bf72d) land here
        /// as `cpu_millicores` / `ram_bytes`. A meter ABSENT here was not
        /// measured — the wire spells that as an absent field, never as 0, and
        /// a reader must never take a 0 as a ceiling. An order that would
        /// raise a product-scope cap past a number here is refused by name;
        /// the box's DECLARED ceilings (`RoomLeft::ceilings`) carry only
        /// for a meter this map does not hold.
        ceilings: BTreeMap<String, u64>,
    },
    /// **It could not be asked, or would not say — which is not zero.** The
    /// string names WHICH: an RPC the deployed build predates, a control plane
    /// that is down, a plugin that was never wired.
    Unmeasured(String),
}

/// One named meter. Names are the product's, documented in its plugin crate, e.g.
/// `pack_bytes`, `cache_bytes`, `lfs_bytes`, `tombstoned_bytes`, `open_store_ram_bytes`,
/// `pushes`, `anonymous_reads`. Values are the product's units, usually bytes or counts.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Usage {
    pub meters: BTreeMap<String, u64>,
    pub measured_at_unix_ms: u64,
}

/// **One page of a BATCHED usage read** — [`pulled::Pulled::read_usage_page`]'s
/// question (2026-10-06).
///
/// The poller used to ask [`pulled::Pulled::read_usage`] once per tenant per tick: at
/// 1,000 tenants that was ~25–30 s of 1.4 cores on gunnar's main every five
/// minutes, linear in tenants, and at ~10,000 the whole period. A page answers
/// many tenants in ONE call, and `changed_since` lets the product answer only
/// the tenants whose usage moved — so a quiet estate costs a few calls, not one
/// per tenant.
///
/// **Who a listed tenant IS** ([`pulled::Pulled::tenant_owners`], 2026-10-08): the product's own
/// word for the namespace's owner, so a SELLING shop can give every PERSON's account a fact
/// (no account there is "no fact = uncapped") and leave the SELLER's own alone.
///
/// Every field defaults, and neither side denies unknown fields.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct TenantOwner {
    pub tenant: TenantId,
    /// The tenant is a PERSON's own account (gunnar: `owner_kind` USER). False for a group,
    /// a clan, a machine's, or a namespace of no principal.
    pub person: bool,
    /// The owner holds a SERVER-scope grant at the product (the operator — the seller): the
    /// shop's own, never a customer. Only the product says so; nothing a customer does sets it.
    pub seller: bool,
}

/// Every field defaults, and neither side denies unknown fields: an older and a
/// newer build of either side read each other's JSON.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePageReq {
    /// `""` asks for the first page; otherwise the previous page's
    /// [`UsagePage::next_page_token`], unchanged.
    #[serde(default)]
    pub page_token: String,
    /// At most this many rows; 0 = the product's own default.
    #[serde(default)]
    pub page_size: u32,
    /// `""` = every tenant. Otherwise a [`UsagePage::cursor`] an earlier read
    /// answered: only the tenants whose usage CHANGED since. A product that can
    /// no longer honour it (it restarted, the day turned) answers every tenant
    /// and says so with [`UsagePage::changed_only`] = false.
    #[serde(default)]
    pub changed_since: String,
}

/// One tenant's row in a [`UsagePage`].
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct TenantUsage {
    pub tenant: TenantId,
    pub usage: Usage,
}

/// **How long a whole keep boot may take** (audit 7, 2026-10-18; lane G9, A7 row 9): the keep
/// medium imported and cloned, the box stopped, the medium's run (the provider's own budget,
/// 45 min at UpCloud), the clone given back and the box started. The shop holds its Upgrade
/// call this long (`monetize_cloud::lay::KEEP_BOOT_WITHIN`), and every product's front waits on
/// it exactly as long (`monetize_embed::fleet::KEEP_BOOT_WITHIN`, Gunnar b74ca801): a front
/// that gave up first would report a failure while the shop still works. ONE constant, here,
/// because the shop's cloud crate and the fronts' embed crate both stand on this one.
pub const KEEP_BOOT_WITHIN: std::time::Duration = std::time::Duration::from_secs(120 * 60);

/// **The mark of a sold estate's tenant on the ONE ledger** (5.4, ONE monetize, G16): a
/// private Gunnar runs no monetize of its own, so its tenants are accounts of product `gunnar`
/// in the ONE ledger as `@<estate>/<namespace>`. `@` is no character of a gunnar namespace
/// (`gunnar-core` sandbox rules), so no tenant of the product's own estate reads as one.
pub const ESTATE_MARK: char = '@';

impl TenantId {
    /// `namespace` on the sold estate `estate`, as the ONE ledger keys it: `@<estate>/<namespace>`.
    pub fn on_estate(estate: &str, namespace: &str) -> Self {
        TenantId(format!("{ESTATE_MARK}{estate}/{namespace}"))
    }

    /// `(estate, namespace)` when this is a sold estate's tenant ([`Self::on_estate`]); `None`
    /// for a tenant of the product's own estate.
    pub fn estate(&self) -> Option<(&str, &str)> {
        let (estate, namespace) = self.0.strip_prefix(ESTATE_MARK)?.split_once('/')?;
        (!estate.is_empty() && !namespace.is_empty()).then_some((estate, namespace))
    }
}

/// **One page of a batched usage read** — [`pulled::Pulled::read_usage_page`]'s
/// answer. See [`UsagePageReq`].
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePage {
    /// The readings, in the product's order. MAY include names that
    /// [`pulled::Pulled::list_tenants`] does not list (a product's own machine
    /// accounts); a caller keys by tenant and ignores the rest.
    #[serde(default)]
    pub rows: Vec<TenantUsage>,
    /// `""` = this was the last page.
    #[serde(default)]
    pub next_page_token: String,
    /// What to pass as [`UsagePageReq::changed_since`] next tick. The same on
    /// every page of one read: it is taken when the read BEGAN, so a change that
    /// lands while the pages are fetched is answered again next time rather than
    /// lost.
    #[serde(default)]
    pub cursor: String,
    /// True when `changed_since` was honoured and `rows` holds only the tenants
    /// that changed; false when every tenant is in the read.
    #[serde(default)]
    pub changed_only: bool,
    /// When the product took the reading this page answers from.
    #[serde(default)]
    pub as_of_unix_ms: u64,
}

/// **One tenant's FLOW for one UTC day** — what MOVED, as against what is held.
///
/// Every number in [`Usage`] is a STOCK reading: it answers *how much is on the
/// product's disk*, and it reads exactly the same for a customer who worked
/// this morning and one who last touched the product in March. monetize could
/// therefore bill nine tenants correctly and not know that six of them had
/// stopped. This is the other kind, and it is what the SILENT and FRICTION
/// lists are made of.
///
/// **The meters are the product's own names**, exactly as [`Usage::meters`] is,
/// and for the same reason: core has never held a product-specific field and
/// must not gain one here. gunnar fills `bytes_in`, `bytes_out`, `pushes`,
/// `fetches`, `lfs_bytes_in`, `lfs_bytes_out`, `refused`, its three
/// `refused_*` reasons, `auth_failures`, `distinct_principals` and the two
/// `last_*_unix_ms` timestamps. Another product will fill something else, and
/// nothing here needs to know.
///
/// **A day with no traffic has NO ROW.** Not a row of zeroes: the caller knows
/// which days it asked for, and zero-filling would put a bar of height zero
/// beside a bar that means *not counted*.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ActivityDay {
    /// Midnight UTC of the day, in Unix milliseconds.
    pub day_unix_ms: u64,
    /// The product's own meter names. See the type's doc.
    pub meters: BTreeMap<String, u64>,
}

/// **A tenant's kept activity, and how far back the product remembers.**
///
/// The second half is not decoration. An empty `days` means one of two opposite
/// things — *nothing happened* or *you asked about a time the product no longer
/// remembers* — and only `retained_days` can tell them apart. A console that
/// drew an empty series as "silent" without it would report every tenant on a
/// freshly installed box as a lapsed customer.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Activity {
    /// Oldest first. Days with no traffic are absent.
    pub days: Vec<ActivityDay>,
    /// How many days the product keeps. `0` when it would not say.
    pub retained_days: u64,
}

/// The verdict monetize pushes back. Product-agnostic; the plugin maps it to the
/// product's own enum (gunnar: `EntitlementState`).
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct EntitlementFact {
    pub tenant: TenantId,
    /// **The ORDER this verdict came from** — the ledger reference
    /// (`<product>/<tenant>/<date>+<caps>`) for a purchase, an operator's own
    /// label (or nothing) for a hand-set fact. The field is still called `plan`
    /// on the wire and in the signed canonical form (`monetize_embed::signing`),
    /// because gunnar verifies that form and a renamed field would invalidate
    /// every signature a deployed gunnar checks; the CONTENT is an order
    /// reference since the plan catalogue went on 2026-09-05.
    pub plan: String,
    pub state: State,
    pub paid_until_unix_ms: Option<u64>,
    /// Per-meter caps the product enforces itself (gunnar: `pack_quota_bytes`,
    /// `explode_budget_bytes`). Absent = product default.
    pub caps: BTreeMap<String, u64>,
    /// `operator` | `payment:<vendor>:<reference>` — lands in the product's attestation log.
    pub source: String,
    /// Ed25519 over the canonical JSON of the fields above, by monetize-server's key.
    /// **The V1 form** — it does NOT cover [`EntitlementFact::issued_unix_ms`], and
    /// that is deliberate: it is the signature an appliance built before
    /// 2026-09-17 computes, and it must keep verifying there for ever. See
    /// [`Self::issued_signature`].
    pub signature: Vec<u8>,
    /// **When monetize issued this verdict — the field that makes a fact good
    /// ONCE.**
    ///
    /// Without it an `EntitlementFact` is replayable for ever: capture a `Paid`
    /// fact, wait for the tenant to lapse, push the captured bytes back, and the
    /// signature still verifies because it is a real signature. `paid_until`
    /// cannot tell the two apart — a lapse keeps the date and moves the ladder —
    /// so the only thing that can is something MONOTONIC inside the signed form.
    /// A product refuses a fact that is not newer than the one it holds
    /// (gunnar: `EntitlementSlot::set`).
    ///
    /// Unix milliseconds, and not a sequence number, for the same reason
    /// [`crate`]'s sibling [`monetize_embed::signing::Snapshot`] chose one: a
    /// clock needs no durable per-tenant counter on monetize's side, so a
    /// restored ledger cannot rewind one and mint facts every appliance in the
    /// field then refuses for ever. One idea in this system, not two.
    ///
    /// `None` is a fact signed before this field existed. It stays legal, and a
    /// product accepts it — until that product has seen ONE stamped fact for the
    /// tenant, after which the unstamped form is a downgrade and is refused.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub issued_unix_ms: Option<u64>,
    /// **The V2 signature**: Ed25519 over the canonical JSON of every field
    /// above INCLUDING `issued_unix_ms`
    /// (`monetize_embed::signing::fact_message_issued`).
    ///
    /// Two signatures and not one, because a fact has to be readable by two
    /// generations of appliance at once. An appliance that predates this field
    /// reads only fields 1-7 off the wire, computes the V1 form, and checks
    /// [`Self::signature`] — so it accepts a stamped fact unchanged, and a
    /// paying customer on an un-upgraded box loses nothing. An appliance that
    /// knows the field checks BOTH, so the issue time is signed and cannot be
    /// added, moved or bumped by whoever relays the fact.
    ///
    /// Empty exactly when `issued_unix_ms` is `None`; neither is legal without
    /// the other.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub issued_signature: Vec<u8>,
}

/// The ladder. Numbers (grace/retention days) are the deployment's policy
/// (`monetize::Policy`), not the enum's.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum State {
    Free,
    Paid,
    Grace,
    Suspended,
    Retention,
}

#[derive(Clone, Debug)]
pub enum ProductError {
    /// The product's control plane refused (auth, unknown tenant).
    Refused(String),
    /// Product unreachable. Retryable; nothing was written.
    Unavailable(String),
}

impl std::fmt::Display for ProductError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ProductError::Refused(r) => write!(f, "product refused: {r}"),
            ProductError::Unavailable(r) => write!(f, "product unavailable: {r}"),
        }
    }
}
impl std::error::Error for ProductError {}

/// **Where the room for a target comes from, as the product measured it** —
/// what [`pulled::Pulled::can_absorb`] answers when it can serve.
///
/// A yes is two different facts, and the difference is money. gunnar's
/// `Admin.Capacity` answers a `verdict` and a `growth`, and the three readings
/// of them a seller can act on are: there is room already (buy nothing), there
/// is no room but a bought disk will be used (buy, and somebody has to extend a
/// filesystem), and there is no room and a bought disk will NOT be used (refuse).
/// The third is a refusal and never reaches this type. Before this type existed
/// a yes was `Ok(())` and the cloud was asked to `ensure` a disk regardless. With
/// the front's configuration (`pool_reserve_gib` covering the whole data set)
/// UpCloud's pool arithmetic answers "short" for every order, so every yes
/// bought and attached a disk to an appliance that had just said it had room —
/// read from the code and the config, not from a bill.
///
/// The transaction turns it into `monetize_cloud::Room` for the provider, so
/// the cloud is told what the product measured and never has to know which
/// product that was.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Absorb {
    /// **Served out of what the product already has.** Nothing may be bought
    /// for it: a disk bought on top of this answer is billed and not needed.
    OnHand,
    /// **Short today, and bought iron becomes usable** — by the hand `note`
    /// names, in the product's own words (gunnar: an operator extends the
    /// filesystem out of band, and gunnar measures the larger one on its next
    /// look). Carried onto the purchase row so the ledger records that
    /// somebody still has a job to do after the disk is attached.
    WithIron { note: String },
}

/// **What a metered verb answered** — see [`pulled::Pulled::metered_door`]. Three
/// arms, because "nobody could ask" is neither a paid build nor a community
/// one, and a deployment that sells against any of the last two is selling
/// something it cannot deliver.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum MeteredDoor {
    /// **The box answered a metered verb.** It has the paid services compiled
    /// in; the string is whatever the plugin wants to record about the proof
    /// (the verb it called, the build it saw).
    Enforcing(String),
    /// **The box answered `UNIMPLEMENTED`.** A community build. Every paid
    /// signup against it will take the money and fail to grant the
    /// entitlement, so a deployment that sells is misconfigured, loudly.
    Absent(String),
    /// **Nobody could ask** — the box is down, the plugin has no probe, nothing
    /// has run yet. NOT a yes and NOT a no; carries which.
    Unproven(String),
}

impl MeteredDoor {
    /// `enforcing` | `absent` | `unproven` — the wire, JSON and log spelling,
    /// one writer.
    pub fn name(&self) -> &'static str {
        match self {
            MeteredDoor::Enforcing(_) => "enforcing",
            MeteredDoor::Absent(_) => "absent",
            MeteredDoor::Unproven(_) => "unproven",
        }
    }
    /// The plugin's own words about the answer.
    pub fn detail(&self) -> &str {
        match self {
            MeteredDoor::Enforcing(s) | MeteredDoor::Absent(s) | MeteredDoor::Unproven(s) => s,
        }
    }
    /// **Has the paid door been PROVEN to be there?** False for `Unproven`,
    /// which is the whole point of the arm: a deployment may not call itself
    /// healthy on the strength of a question nobody answered.
    pub fn proven(&self) -> bool {
        matches!(self, MeteredDoor::Enforcing(_))
    }
}

/// **One TEST BUTTON a product declares** (owner 2026-10-11): the operator's
/// console renders one button per declared action and monetize runs it through
/// the SAME sign-up / estate-order path a customer takes, on a vendor in test
/// mode only. Data, never markup: the console stays a thin renderer and the
/// product never ships HTML into it.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TestAction {
    /// Stable within the product (`hosted-10gb`); what the console sends back.
    pub id: String,
    /// What the button reads (`Test: 10 GB hosted`).
    pub label: String,
    pub kind: TestActionKind,
}

/// **What a test button buys** — the two things a stranger can buy today.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TestActionKind {
    /// A hosted account with this much room, at the plan the sign-up page
    /// offers for it NOW (`Signup.Plans`) — the product names the room, never a
    /// priced id, because the price is the shop's.
    HostedPlan { cap_gib: u64 },
    /// A private instance from the estate-order ladder, at these sizes (empty:
    /// the ladder's own default for that field).
    PrivateInstance {
        #[serde(default)]
        front_plan: String,
        #[serde(default)]
        main_plan: String,
        #[serde(default)]
        main_data_gib: u64,
    },
}

/// **What is fixed of a product, as DATA** (A32-13): its manifest's id and meters and its facts'
/// tickets, test buttons and guardian. The host reads it ONCE ([`pulled::Pulled::facts`]) and holds it
/// in the product's slot, as the Cloud and Payment slots hold their manifests; every reader reads
/// the slot ([`Served`]), never the product.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ProductFacts {
    /// `gunnar`, `holger`: the block's id. Also the first segment of every payment reference.
    pub id: &'static str,
    /// The meters this product declares — the UI and the order form read this, so an order
    /// can only cap a meter that exists, and the console can say whether a cap on it is
    /// policed ([`Enforcement`]).
    pub meters: &'static [Meter],
    /// **Its main mints actor tickets** (the facts' `tickets`): a console may offer
    /// self-service renewal. False means there is nothing that could tell a customer from a
    /// stranger, so the route must not exist rather than existing and refusing. The HOST checks
    /// each ticket. Reported on the wire as `ProductInfo.tenant_renewal`.
    pub tickets: bool,
    /// **The test buttons this product offers its operator** ([`TestAction`]). Empty: the
    /// console shows none for it.
    pub test_actions: Vec<TestAction>,
    /// **It brings an estate guardian** ([`guardian::EstateGuardian`], 5.4): how its estates are
    /// diagnosed and repaired. The host holds the guardian beside the slot.
    pub guardian: bool,
    /// `Some(why)`: the facts could not be read yet (a guest that could not answer); its
    /// tickets, test buttons and guardian are off, and the host asks again on the next look.
    pub unread: Option<String>,
}

impl ProductFacts {
    /// A product `id` with `meters` and nothing else: no tickets, no buttons, no guardian.
    pub fn new(id: &'static str, meters: &'static [Meter]) -> ProductFacts {
        ProductFacts {
            id,
            meters,
            ..Default::default()
        }
    }

    /// **These facts, interned**: one `'static` copy per distinct answer (the meter tables are
    /// interned the same way), so a slot hands out `&'static` facts that outlive a replaced
    /// guest, and reading the same answer again leaks nothing.
    pub fn leak(self) -> &'static ProductFacts {
        static HELD: std::sync::Mutex<Vec<&'static ProductFacts>> =
            std::sync::Mutex::new(Vec::new());
        let mut held = HELD.lock().unwrap_or_else(|e| e.into_inner());
        if let Some(f) = held.iter().find(|f| ***f == self) {
            return f;
        }
        let f: &'static ProductFacts = Box::leak(Box::new(self));
        held.push(f);
        f
    }
}

/// **A product as the host serves it**: its facts, read once, beside the product's estate verbs.
/// Copy, like `monetize_pay::Vendor`: the slot's facts (`&'static`) and the product. Every
/// reader of an id, a meter table, the ticket fact or the test buttons reads them here; the
/// estate verbs are the product's ([`std::ops::Deref`]).
#[derive(Clone, Copy)]
pub struct Served<'a> {
    facts: &'a ProductFacts,
    product: &'a pulled::Pulled,
    guardian: Option<&'a dyn guardian::EstateGuardian>,
    /// **The room a quote sells against**, when the host decides it (the queue past the room,
    /// monetize-server `grow::can_absorb_queued`); `None`: the product's own answer.
    absorb: Option<AbsorbFn<'a>>,
}

/// The host's own answer to a quote's room ([`Served::with_room`]).
pub type AbsorbFn<'a> =
    &'a (dyn Fn(&TenantId, &BTreeMap<String, u64>) -> Result<Absorb, String> + Sync);

impl<'a> Served<'a> {
    /// `product` with the facts the host holds for it, and no guardian.
    pub fn new(facts: &'a ProductFacts, product: &'a pulled::Pulled) -> Served<'a> {
        Served {
            facts,
            product,
            guardian: None,
            absorb: None,
        }
    }
    /// **The same, the quote's room decided by the host** (MONETIZE-FIX step 3, replacing the
    /// `Growing` wrapper): `absorb` answers whether an order fits.
    pub fn with_room(mut self, absorb: AbsorbFn<'a>) -> Served<'a> {
        self.absorb = Some(absorb);
        self
    }
    /// **Whether `caps` fit for `tenant`**: the host's room when it decides it, else the product's.
    pub fn can_absorb(
        &self,
        tenant: &TenantId,
        caps: &BTreeMap<String, u64>,
    ) -> Result<Absorb, String> {
        match self.absorb {
            Some(f) => f(tenant, caps),
            None => self.product.can_absorb(tenant, caps),
        }
    }
    /// The same, with the estate guardian the host holds beside it.
    pub fn with_guardian(
        mut self,
        guardian: Option<&'a dyn guardian::EstateGuardian>,
    ) -> Served<'a> {
        self.guardian = guardian;
        self
    }
    /// **The estate guardian that answers for it** (5.4): how its estates are diagnosed and
    /// repaired. `None`: monetize repairs none of its estates.
    pub fn guardian(&self) -> Option<&'a dyn guardian::EstateGuardian> {
        self.guardian
    }
    /// The block's id.
    pub fn id(&self) -> &'static str {
        self.facts.id
    }
    /// The declared meters.
    pub fn meters(&self) -> &'static [Meter] {
        self.facts.meters
    }
    /// Its main mints actor tickets ([`ProductFacts::tickets`]).
    pub fn tenant_renewal(&self) -> bool {
        self.facts.tickets
    }
    /// Its operator's test buttons.
    pub fn test_actions(&self) -> &'a [TestAction] {
        &self.facts.test_actions
    }
    /// Everything the host holds of it.
    pub fn facts(&self) -> &'a ProductFacts {
        self.facts
    }
    /// The product's estate verbs, without the facts.
    pub fn product(&self) -> &'a pulled::Pulled {
        self.product
    }
}

impl<'a> std::ops::Deref for Served<'a> {
    type Target = pulled::Pulled;
    fn deref(&self) -> &Self::Target {
        self.product
    }
}

impl std::fmt::Debug for Served<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Served")
            .field("id", &self.facts.id)
            .finish()
    }
}

/// **What the host says about a ticket somebody presented** (`monetize_server::site_ticket`).
///
/// Not a `bool` and not a `Result<(), ProductError>`: a refusal is a normal,
/// expected answer that carries a SENTENCE — the words a caller may show — and
/// a `ProductError` means something else entirely (the product's control plane
/// is down, the question could not be asked). Conflating the two would let an
/// unreachable appliance read as a refused ticket, or worse, the reverse.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ActorVerdict {
    /// The ticket verifies, names this tenant, this product and this verb, is
    /// inside its life, and has not been spent before.
    Allowed,
    /// Refused, and why — in words a customer surface may show. A verifier must
    /// not name the ticket's own tenant back to whoever asked about another
    /// one; see `monetize_embed::verify_ticket`.
    Refused(String),
}

/// **"Add users"** (owner, 2026-10-04): `count` users, each holding
/// `room_bytes` of the tenant's room. `count` 1 is ONE named user (`name`);
/// more is that many SEATS with no names, filled later (an invite, an
/// import). `room_bytes` 0 is the product's own default. The rules a person
/// types against (the range, the names, the room's floor) are the PRODUCT's,
/// which every door shares.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAsk {
    pub count: u32,
    pub room_bytes: u64,
    #[serde(skip_serializing_if = "String::is_empty")]
    pub name: String,
    /// Where the named user's invite is mailed; empty: nowhere. Not part of
    /// the ask's identity ([`UsersAsk::fingerprint`]).
    #[serde(skip_serializing_if = "String::is_empty")]
    pub email: String,
    /// The console the invite is redeemed at.
    #[serde(skip_serializing_if = "String::is_empty")]
    pub invite_origin: String,
    /// **The named user as the ledger knows them** (G13, 5.4): an opaque id
    /// (`monetize::ledger::principal_id`), never the name. Empty for seats.
    #[serde(skip_serializing_if = "String::is_empty")]
    pub principal_id: String,
}

impl UsersAsk {
    /// **What a ticket for this ask covers** — the "caps" an `order` actor
    /// ticket is minted over and compared against, so a ticket vouches for
    /// exactly this number of users at exactly this room, and no other.
    pub fn ticket_caps(&self) -> BTreeMap<String, u64> {
        [
            ("users".to_owned(), u64::from(self.count.max(1))),
            ("user_room_bytes".to_owned(), self.room_bytes),
        ]
        .into_iter()
        .collect()
    }

    /// A short, stable fingerprint for a reference: `2x10737418240` or
    /// `1x10737418240:p-<id>` — the named user by [`UsersAsk::principal_id`],
    /// never by name (G13).
    pub fn fingerprint(&self) -> String {
        match self.principal_id.as_str() {
            "" => format!("{}x{}", self.count.max(1), self.room_bytes),
            id => format!("{}x{}:{id}", self.count.max(1), self.room_bytes),
        }
    }

    /// **The ask as the ledger may hold it**: no name, no address — the
    /// named user only by [`UsersAsk::principal_id`].
    pub fn without_names(&self) -> UsersAsk {
        UsersAsk {
            name: String::new(),
            email: String::new(),
            ..self.clone()
        }
    }

    /// One named user (the name, or its id, is known).
    pub fn is_named(&self) -> bool {
        !self.name.is_empty() || !self.principal_id.is_empty()
    }
}

/// The product's answer to [`pulled::Pulled::users_fit`] / [`pulled::Pulled::add_users`].
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAnswer {
    /// The named user, once created; empty for seats and for a dry run.
    pub principal: String,
    /// Bytes each user holds, as the product resolved it (its default applied).
    pub room_bytes: u64,
    /// What the ask needs of the tenant's room: count × room.
    pub need_bytes: u64,
    /// The tenant's room neither stored nor promised, before the ask. `None`:
    /// nothing caps the tenant there.
    pub unused_bytes: Option<u64>,
    /// How much is missing; 0 = it fits.
    pub shortfall_bytes: u64,
    /// The tenant's seats after this: every seat, and the open ones.
    pub seats: u32,
    pub seats_open: u32,
    /// Nothing was written: a dry run, or the retry of a granted order.
    pub unchanged: bool,
    /// The new named user's invite secret, on the answer that created the
    /// user (and never again): what a `<console>/login?invite=` link carries.
    #[serde(skip_serializing_if = "String::is_empty")]
    pub invite_secret: String,
}

/// What [`pulled::Pulled::purge_tenant`] did.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Purged {
    pub tenant: TenantId,
    /// What the product removed, in its own words and its own units — "5 of 5
    /// stores, 1.2 GiB". Free text because every product counts different
    /// things and a shared schema would force each of them to lie a little.
    pub detail: String,
    /// Bytes the product says it reclaimed. `None` means it does not count
    /// them, which is not the same as zero and must not be rendered as it.
    pub bytes_reclaimed: Option<u64>,
    /// **Is the tenant still locked out of the product?**
    ///
    /// `true` is a real and expected outcome — an operator may have suspended
    /// the account separately, and the purge correctly refuses to lift a
    /// lockout it did not take — but it is also what a purge that could not
    /// unlock reports, and either way the tenant cannot use what is left.
    /// Carried so a super purge can say so rather than infer it.
    pub still_locked: bool,
    /// One line per thing the product could not remove. Non-empty means the
    /// tenant's data is PARTIALLY there, and the caller must not go on to
    /// destroy the iron it is sitting on.
    pub failures: Vec<String>,
}

/// **Bytes in the largest unit that still leaves a digit before the decimal
/// point**, by integer arithmetic: `10.0 GiB`, `64.0 MiB`, `999 B`.
///
/// One writer, because there used to be three and every one of them carried the
/// same bug. Each divided by 1 GiB unconditionally and printed one decimal, so
/// anything under a gibibyte rendered `0.0 GiB` — and `0.0` beside a quantity is
/// read as *there is none*:
///
/// * the console (then a Plans page) showed a real 64 MiB
///   `pack_bytes` cap as `0.0 GiB`, which an operator reads as "no quota";
/// * the old catalogue validator said *"sells pack_bytes = 10.0 GiB but
///   its resources buy only 10.0 GiB — short 0.0 GiB"*, a sentence in which
///   every number is wrong in the direction of "nothing is the matter";
/// * `products/gunnar`'s capacity refusals said "short 0.0 GiB" for the same
///   reason.
///
/// It lives here because this is the crate that already owns the meter
/// vocabulary ([`Meter`], [`Backing::DiskBytes`]) and the only one every side
/// can depend on: core, the product plugins, and the browser console alike. It
/// pulls in nothing (this crate is `serde` and nothing else), so the wasm
/// bundle pays a few hundred bytes for a formatter it was carrying anyway.
///
/// Truncates, never rounds up: a reading must not appear to cross a cap it has
/// not crossed.
pub fn bytes(n: u64) -> String {
    const KIB: u64 = 1 << 10;
    const MIB: u64 = 1 << 20;
    const GIB: u64 = 1 << 30;
    const TIB: u64 = 1 << 40;
    let (unit, per) = match n {
        n if n >= TIB => ("TiB", TIB),
        n if n >= GIB => ("GiB", GIB),
        n if n >= MIB => ("MiB", MIB),
        n if n >= KIB => ("KiB", KIB),
        // Under a kibibyte there is nothing to scale to, and `0.0 KiB` would be
        // the same lie one unit down. Bytes are exact and short.
        n => return format!("{n} B"),
    };
    format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
}

#[cfg(test)]
mod backing_tests {
    use super::Backing;

    /// **A byte over the GiB is sold the next GiB, and a millicore is priced
    /// as a thousandth of a core.** Distinct non-zero numbers: 2200 öre per
    /// GiB-month, 90 GiB + 1 byte, 1500 millicores at 30 000.
    #[test]
    fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
        const GIB: u64 = 1 << 30;
        assert_eq!(
            Backing::DiskBytes.price_month(90 * GIB, 2200),
            198_000,
            "90 GiB × 22.00 SEK"
        );
        assert_eq!(
            Backing::DiskBytes.price_month(90 * GIB + 1, 2200),
            200_200,
            "one byte over is the 91st GiB"
        );
        assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
        assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
        assert_eq!(
            Backing::CpuMillicores.price_month(1500, 30_000),
            45_000,
            "1.5 cores at 300.00"
        );
        assert_eq!(
            Backing::CpuMillicores.price_month(1, 30_000),
            30,
            "one millicore is not free and not a core"
        );
        // A counted unit is flat: five properties at 200.00 SEK-month are 1000.00,
        // and one is 200.00 — no rounding, no thousandths. This is the arm a per-property
        // subscription rides on; before it existed a property priced to zero.
        assert_eq!(Backing::Units.price_month(5, 20_000), 100_000, "5 × 200.00");
        assert_eq!(Backing::Units.price_month(1, 20_000), 20_000);
        assert_eq!(Backing::Units.price_month(0, 20_000), 0);
        assert_eq!(Backing::Units.unit(), "unit_month");
        // A zero unit price is FREE, whatever the delta: the open-source list.
        for b in Backing::ALL {
            assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
        }
        // The unit names are the price list's keys, both ways.
        for b in Backing::ALL {
            assert_eq!(Backing::from_unit(b.unit()), Some(b));
        }
        assert_eq!(Backing::from_unit("moon_month"), None);
    }
}

#[cfg(test)]
mod bytes_tests {
    use super::bytes;

    /// **A quantity that exists never renders as zero.** RED before this
    /// function existed: `0.0 GiB` for every value under 2^30, in three separate
    /// copies of the same six lines.
    #[test]
    fn only_a_genuine_zero_reads_as_zero() {
        for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
            let s = bytes(n);
            assert!(
                !s.starts_with("0.0 ") && !s.starts_with("0 "),
                "{n} bytes rendered as {s:?}, which reads as nothing"
            );
        }
        assert_eq!(bytes(0), "0 B");
    }

    #[test]
    fn the_unit_scales_and_the_value_truncates() {
        assert_eq!(bytes(999), "999 B");
        assert_eq!(bytes(1536), "1.5 KiB");
        assert_eq!(bytes(67_108_864), "64.0 MiB");
        assert_eq!(
            bytes((1 << 30) - 1),
            "1023.9 MiB",
            "truncates, never rounds up past the cap"
        );
        assert_eq!(bytes(10 << 30), "10.0 GiB");
        assert_eq!(bytes(3 << 40), "3.0 TiB");
        assert_eq!(
            bytes(u64::MAX),
            "16777215.9 TiB",
            "no overflow at the top of the range"
        );
    }
}

#[cfg(test)]
mod estate_tenant_tests {
    use super::*;

    /// ★ G16: a sold estate's tenant is `@<estate>/<namespace>` on the ONE ledger and reads
    /// back whole (a namespace keeps its own slashes); a tenant of the product's own estate,
    /// nested or not, never reads as one, and a half-mark is no estate.
    #[test]
    fn a_sold_estates_tenant_carries_the_estate_in_its_path_and_no_own_tenant_reads_as_one() {
        let t = TenantId::on_estate("acme", "bob");
        assert_eq!(t.0, "@acme/bob");
        assert_eq!(t.estate(), Some(("acme", "bob")));
        assert_eq!(
            TenantId::on_estate("acme", "team/sub").estate(),
            Some(("acme", "team/sub"))
        );
        for own in [
            "bob",
            "team/sub",
            "acme/bob",
            "",
            "@acme",
            "@/bob",
            "@acme/",
            "x@acme/bob",
        ] {
            assert_eq!(TenantId(own.into()).estate(), None, "{own:?}");
        }
    }
}