onetaskgraph-github-projects 0.2.22

A onetaskgraph source over GitHub Projects.
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
//! What a session of requests to GitHub cost, counted rather than argued about.
//!
//! Nothing here decides anything: it records what left this crate and adds it up. It
//! exists because a query strategy cannot be chosen between without measuring what each
//! one costs.
//!
//! # What one record carries
//!
//! One [`Request`] per outgoing HTTP request. A GraphQL request is named by the document
//! it sent, read out of [`graphql::DOCUMENTS`] rather than from
//! a second list of names, and carries that document's worst-case node count under the
//! bindings that request actually sent — [`node_count`], the same
//! offline calculation `tests/node_count.rs` holds every document to, never a second
//! arithmetic. A REST request sends no document and has no node count, so it names the
//! endpoint it addressed instead. Both record whether they read or wrote, how they ended,
//! and the rate-limit facts that response's own headers carried.
//!
//! **Two quantities of GitHub's, kept apart by name.** `nodeCount` is the most nodes *one
//! query may return*, checked per query; it is what [`Call::Document`] carries.
//! `cost` is rate-limit points, metered per hour across everything one credential does; it
//! is what [`Spend`] is in. A document well under the node limit says nothing about the
//! second.
//!
//! # How a session's spend is arrived at, and what it is not
//!
//! Per budget, accumulated per call, from whatever that call itself makes attributable:
//!
//! - **[`Budget::Rest`] is metered in requests,** so a call is its own measure and is
//!   attributed one request ([`Basis::Counted`]).
//! - **[`Budget::Graphql`] is metered in points.** Where the request was shaped so GitHub
//!   reports its own `cost` — a document selecting `rateLimit { cost }` — that is what is
//!   attributed ([`Basis::Reported`]). Otherwise this repository's stated cost model
//!   applies: **GitHub charges at least one point for any call, so one point is
//!   attributed** ([`Basis::Modelled`]), and the report says how much of the total came
//!   that way. That is a **lower bound** and the accounting says so rather than implying a
//!   measurement: a call over a large connection really costs more, and no document this
//!   source sends today asks GitHub what.
//! - **A rate-limited refusal is attributed nothing** ([`Basis::NotRun`]), because a
//!   request GitHub refused for a rate limit did not run — the same reading of a refusal
//!   that makes retrying one safe in [`GitHubProjectsSource::graphql`](crate::GitHubProjectsSource).
//!
//! **What a session spent is never inferred by differencing a shared counter.** This
//! account is shared and rate-limited, and other work draws on the same budgets in the same
//! window, so an allowance that fell by sixty while this session made ten calls measures
//! the account rather than the session. The report gives that movement anyway — it is worth
//! seeing — and says on its face that it is the account's and not this session's.
//!
//! # Where a reader finds the report
//!
//! [`Session::report`] renders one from a snapshot, and the credentialed lane in
//! `tests/live.rs` prints it at the end of every run, passed or failed — from a `Drop`, so
//! that the run whose cost is most worth reading, the one that broke, is not the run that
//! skips it. It carries no credential, no token, no issue body and no board content: a call
//! is named by a document description this crate wrote or by an [`Endpoint`], which is a
//! path template rather than the URL a run built, and everything else in it is a number.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::sync::Mutex;

use serde_json::Value;

pub use reqwest::StatusCode;

use crate::{Limiter, Variables, graphql, largest_page_sizes, node_count};

/// Which of GitHub's two separately metered budgets a request drew on.
///
/// They are two because GitHub meters them separately, and a single total would hide which
/// one is close to exhaustion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Budget {
    /// The GraphQL API, metered in points.
    Graphql,
    /// The REST API, metered in requests.
    Rest,
}

impl Budget {
    /// This budget's name, as GitHub's own `x-ratelimit-resource` header spells it.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Graphql => "graphql",
            Self::Rest => "rest",
        }
    }
    /// What GitHub meters this budget in.
    #[must_use]
    pub const fn unit(self) -> &'static str {
        match self {
            Self::Graphql => "points",
            Self::Rest => "requests",
        }
    }
}

/// Whether a request read or wrote.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Mode {
    /// It asked for something.
    Read,
    /// It changed something.
    Write,
}

impl Mode {
    /// What a GraphQL document does.
    ///
    /// Every mutation this source sends creates content and no query does, so the keyword
    /// is the whole of the question — the same test [`crate::GitHubProjectsSource`] paces
    /// its own writes by.
    #[must_use]
    pub fn of_document(document: &str) -> Self {
        if crate::is_mutation(document) {
            Self::Write
        } else {
            Self::Read
        }
    }
    /// This mode's name in a report.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Read => "read",
            Self::Write => "write",
        }
    }
}

/// The HTTP methods a REST call to GitHub is made with.
///
/// A closed set rather than a string: a record cannot then carry a method that is not one,
/// and a misspelling is refused by [`Method::parse`] where it happens rather than quietly
/// counted as a write. It is this crate's own enum rather than an HTTP client's, so which
/// client a caller sends its own requests with stays the caller's business.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Method {
    /// `GET`.
    Get,
    /// `HEAD`.
    Head,
    /// `POST`.
    Post,
    /// `PUT`.
    Put,
    /// `PATCH`.
    Patch,
    /// `DELETE`.
    Delete,
}

impl Method {
    /// The method `name` spells, whatever its case, or `None` when it spells none of them.
    #[must_use]
    pub fn parse(name: &str) -> Option<Self> {
        Some(match name.trim().to_ascii_uppercase().as_str() {
            "GET" => Self::Get,
            "HEAD" => Self::Head,
            "POST" => Self::Post,
            "PUT" => Self::Put,
            "PATCH" => Self::Patch,
            "DELETE" => Self::Delete,
            _ => return None,
        })
    }
    /// Its name, as HTTP spells it.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Head => "HEAD",
            Self::Post => "POST",
            Self::Put => "PUT",
            Self::Patch => "PATCH",
            Self::Delete => "DELETE",
        }
    }
    /// Whether it reads or writes.
    #[must_use]
    pub const fn mode(self) -> Mode {
        match self {
            Self::Get | Self::Head => Mode::Read,
            Self::Post | Self::Put | Self::Patch | Self::Delete => Mode::Write,
        }
    }
}

/// How one request ended.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Outcome {
    /// GitHub answered with what was asked for.
    Answered,
    /// GitHub did not answer it, for a reason that is not a rate limit — including a
    /// request that never reached GitHub at all, which carries no headers to read.
    Refused,
    /// A rate limiter refused it, so it never ran.
    RateLimited,
}

impl Outcome {
    /// How a response ended, read the way this source's own limiter reads one.
    ///
    /// `budget_exhausted` is whether `x-ratelimit-remaining` was exactly `0`, which
    /// *explains* a failing response rather than making a successful one fail. A GraphQL
    /// success carrying `errors` is a refusal only its caller can rule on, so this answers
    /// [`Outcome::Answered`] for one and the caller narrows it.
    ///
    /// The status is a [`StatusCode`] rather than a number, so a status HTTP has no room
    /// for cannot be asked about at all — and an HTTP client has already parsed one for
    /// every response it hands back, so nothing is asked of a caller that it did not have.
    #[must_use]
    // llmlint: ignore[invalid_states_unrepresentable] `budget_exhausted` is one header read as the yes-or-no it is — whether `x-ratelimit-remaining` was exactly `0` — so both of its values are meaningful and there is no third state a type could forbid. It is also the argument [`Limiter::classify`] already takes, on the line below, so giving this one wrapper its own spelling would mean two vocabularies for one header rather than one.
    pub fn of_response(status: StatusCode, budget_exhausted: bool, body: &str) -> Self {
        if Limiter::classify(status, budget_exhausted, body).is_some() {
            return Self::RateLimited;
        }
        if status.is_success() {
            Self::Answered
        } else {
            Self::Refused
        }
    }
    /// This outcome's name in a report.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Answered => "answered",
            Self::Refused => "refused",
            Self::RateLimited => "rate-limited",
        }
    }
}

/// The rate-limit facts one response's own headers carried.
///
/// Every figure is optional because every one of them is absent from some real response: a
/// request that never reached GitHub has no headers at all, and a refusal from an
/// intermediary carries whichever of them that intermediary felt like carrying. An absent
/// figure is reported as unknown rather than guessed at.
///
/// **These are observations of one response, so the only ways to have one are to observe a
/// response ([`RateLimit::read`]) or to have observed nothing ([`RateLimit::default`]).**
/// The fields are read through the accessors below rather than assembled: a hand-built set
/// could say the account had more remaining than its whole allowance, or name a resource
/// GitHub would never name, and a report built on it would be a measurement of nothing.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RateLimit {
    /// `x-ratelimit-limit`: the whole allowance for this budget's window.
    limit: Option<u64>,
    /// `x-ratelimit-remaining`: what was left of it when GitHub answered.
    remaining: Option<u64>,
    /// `x-ratelimit-used`: what the *account* had spent, which is not what this session
    /// spent — other work shares the budget.
    used: Option<u64>,
    /// `x-ratelimit-reset`: the Unix second the allowance comes back.
    reset: Option<u64>,
    /// `x-ratelimit-resource`: which budget GitHub says these figures are about.
    resource: Option<String>,
}

impl RateLimit {
    /// Read the five headers GitHub carries a budget's state in.
    ///
    /// `header` is given a lower-case header name and answers that response's value for
    /// it. Taking a lookup rather than a header map is what keeps the HTTP client this
    /// crate happens to use out of its public interface, so a caller sending its own
    /// requests with its own client records into the same accounting.
    #[must_use]
    pub fn read(header: impl Fn(&str) -> Option<String>) -> Self {
        let number = |name: &str| {
            header(name)
                .as_deref()
                .map(str::trim)
                .and_then(|value| value.parse::<u64>().ok())
        };
        // The three allowance figures are one fact and are read as one. A response saying
        // more was left of a budget than the whole of it, or more of it used than it holds,
        // cannot be true of any account — and a report built on it would be a measurement of
        // nothing, which is what the type documentation above refuses. So a set that cannot
        // all be true is a budget state this response did not carry: dropped **together**,
        // because which of the three is the wrong one is not knowable from here, and
        // reported as unknown rather than repaired into a figure nothing observed. `reset`
        // and `resource` are independent of them and survive. `Allowance::read` in
        // `onetaskgraph-live` refuses the same impossibility where the gate reads it.
        let (limit, remaining, used) = (
            number("x-ratelimit-limit"),
            number("x-ratelimit-remaining"),
            number("x-ratelimit-used"),
        );
        let over_the_whole = |figure: Option<u64>| match (figure, limit) {
            (Some(figure), Some(limit)) => figure > limit,
            _ => false,
        };
        // And the same impossibility one step on, which neither figure shows on its own:
        // `used` and `remaining` are two views of one allowance — what the window has spent
        // and what is left of it — so between them they cannot exceed the whole. A response
        // saying 4,000 used and 4,000 left of 5,000 has each figure inside the limit and
        // still cannot be true of any account.
        //
        // **Only that direction is refused, and a sum falling short of the whole is kept
        // deliberately.** It accounts for less of the budget than exists rather than for
        // more of it than could, which no arithmetic forbids and which a response really
        // carries: the loopback board this crate is tested against answers `remaining: 0`
        // beside a `used` naming what that session itself spent, wherever it stands in for a
        // budget somebody else exhausted. Dropping those three would report the budget as
        // unknown, which is a worse answer than a short one — and it could not reach what
        // the report says this SESSION spent in any case, because that figure is attributed
        // per call and is never differenced out of these.
        let more_than_the_whole_between_them = match (used, remaining, limit) {
            (Some(used), Some(remaining), Some(limit)) => used.saturating_add(remaining) > limit,
            _ => false,
        };
        let readable = !over_the_whole(remaining)
            && !over_the_whole(used)
            && !more_than_the_whole_between_them;
        Self {
            limit: limit.filter(|_| readable),
            remaining: remaining.filter(|_| readable),
            used: used.filter(|_| readable),
            reset: number("x-ratelimit-reset"),
            // The one field here that is not a number, so the one that could carry a third
            // party's arbitrary bytes into a value this crate hands back. GitHub names these
            // `core`, `graphql`, `search`, `integration_manifest`; anything that is not
            // spelled like one is dropped rather than stored.
            resource: header("x-ratelimit-resource")
                .map(|value| value.trim().to_owned())
                .filter(|value| is_resource_name(value)),
        }
    }
    /// The whole allowance for this budget's window, as this response reported it.
    #[must_use]
    pub const fn limit(&self) -> Option<u64> {
        self.limit
    }
    /// What was left of it when GitHub answered.
    #[must_use]
    pub const fn remaining(&self) -> Option<u64> {
        self.remaining
    }
    /// What the **account** had spent — not what this session spent, because other work
    /// draws on the same budget in the same window.
    #[must_use]
    pub const fn used_by_the_account(&self) -> Option<u64> {
        self.used
    }
    /// The Unix second the allowance comes back.
    #[must_use]
    pub const fn reset(&self) -> Option<u64> {
        self.reset
    }
    /// Which budget GitHub said these figures were about, when it named one it spells.
    #[must_use]
    pub fn resource(&self) -> Option<&str> {
        self.resource.as_deref()
    }
    /// Whether these headers say the budget is exactly spent, which is what
    /// [`Outcome::of_response`] reads.
    #[must_use]
    pub fn exhausted(&self) -> bool {
        self.remaining == Some(0)
    }
}

/// Whether a value is spelled like one of GitHub's rate-limit resource names.
///
/// ASCII letters, digits, underscores and hyphens, and short. It is a shape rather than a
/// list because GitHub adds resources — `code_search` arrived after `search` — and a name
/// this does not recognise should be reported as GitHub sent it rather than dropped for
/// being new.
fn is_resource_name(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 64
        && value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
}

/// Where one call's attributed spend came from.
///
/// It is on the record rather than folded into the number so a report can say how much of
/// a session's total is GitHub's own figure and how much is this repository's model.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Basis {
    /// The budget is metered in requests, so the call is its own measure.
    Counted,
    /// GitHub reported this call's own cost, because the request asked it to.
    Reported,
    /// This repository's stated cost model: one point, GitHub's documented minimum for any
    /// call, which is a lower bound rather than a measurement.
    Modelled,
    /// A rate limiter refused the request, so it never ran and is attributed nothing.
    NotRun,
}

impl Basis {
    /// This basis in a report, as a phrase that says what the figure is worth.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Counted => "counted",
            Self::Reported => "reported by GitHub",
            Self::Modelled => "this repository's one-point-per-call lower bound",
            Self::NotRun => "not run",
        }
    }
}

/// What one call is attributed against its budget, and where that figure came from.
///
/// **The amount and the basis are one fact, so they are settled together and read apart.**
/// Three of the four bases fix the amount outright — a call against a budget metered in
/// requests is one request, the model's lower bound is one point, and a request a rate
/// limiter refused never ran and is nothing — and only [`Basis::Reported`] carries a figure
/// of its own, GitHub's. Constructing the pair field by field would let a report say a call
/// GitHub never ran spent forty points, which is a measurement of nothing; the four
/// constructors below are the only ways to have one, and each is the invariant for its own
/// basis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Spend {
    amount: u64,
    basis: Basis,
}

impl Spend {
    /// One request against a budget metered in requests.
    const fn counted() -> Self {
        Self {
            amount: 1,
            basis: Basis::Counted,
        }
    }
    /// What GitHub itself reported this call cost.
    const fn reported(cost: u64) -> Self {
        Self {
            amount: cost,
            basis: Basis::Reported,
        }
    }
    /// This repository's lower bound: one point, GitHub's documented minimum for any call.
    const fn modelled() -> Self {
        Self {
            amount: 1,
            basis: Basis::Modelled,
        }
    }
    /// Nothing, because a rate limiter refused the request and it never ran.
    const fn not_run() -> Self {
        Self {
            amount: 0,
            basis: Basis::NotRun,
        }
    }
    /// The amount, in that budget's own unit.
    #[must_use]
    pub const fn amount(self) -> u64 {
        self.amount
    }
    /// What makes it that amount.
    #[must_use]
    pub const fn basis(self) -> Basis {
        self.basis
    }
}

/// A REST endpoint, spelled the way GitHub's own documentation spells one.
///
/// `GET /repos/{owner}/{repo}/labels`: a method and a path template whose segments are
/// literals or `{placeholder}`s, never the URL a run built from it. That is what makes two
/// runs' reports compare line for line, and it is why this is a type with one constructor
/// rather than a string a caller fills in — a record holding
/// `https://api.github.com/repos/octo-org/board/labels?per_page=100` would put a board's
/// name, and whatever else a query string carried, into a report that promises to carry
/// neither.
///
/// **What it rules on, and what it cannot.** [`Endpoint::parse`] refuses a host, a query
/// string, a fragment, whitespace, a byte outside the small set a path template is spelled
/// from, and anything long — every way an addressed URL differs in *shape* from a template.
/// It cannot tell a template's literal segment from a filled-in one, because
/// `/repos/octo-org/board/labels` is spelled exactly like a template of literals; keeping
/// the placeholders unfilled is the caller's, and the reason `Request::rest`'s own
/// documentation says to pass the template.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Endpoint {
    method: Method,
    path: String,
    name: String,
}

impl Endpoint {
    /// The endpoint `path` spells under `method`, or `None` when `path` is not spelled like
    /// a path template at all.
    ///
    /// Refusing it here is refusing it where it is written, which is the same answer
    /// [`Method::parse`] gives a method HTTP has no verb for: a caller learns at the call
    /// site rather than finding a URL in a report that was supposed to hold none.
    #[must_use]
    pub fn parse(method: Method, path: &str) -> Option<Self> {
        if !is_path_template(path) {
            return None;
        }
        Some(Self {
            method,
            path: path.to_owned(),
            name: format!("{} {path}", method.name()),
        })
    }
    /// The method it is addressed with.
    #[must_use]
    pub const fn method(&self) -> Method {
        self.method
    }
    /// The path template, without the method.
    #[must_use]
    pub fn path(&self) -> &str {
        &self.path
    }
    /// What it is called in a report: the method and the template, as above.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }
}

/// Whether `path` is spelled like one of GitHub's documented endpoint templates.
///
/// An absolute path of `/`-separated segments, each a literal of the bytes a documented
/// endpoint uses or a single `{placeholder}`. A host, a query string, a fragment, an empty
/// segment and anything over the length GitHub's own longest endpoint needs are all refused,
/// because each of them is a way an addressed URL — which carries what a run touched —
/// differs from the template it was built from.
fn is_path_template(path: &str) -> bool {
    if path.is_empty() || path.len() > 128 || !path.starts_with('/') {
        return false;
    }
    path[1..].split('/').all(|segment| {
        let placeholder = segment
            .strip_prefix('{')
            .and_then(|rest| rest.strip_suffix('}'));
        match placeholder {
            Some(name) => {
                !name.is_empty()
                    && name
                        .bytes()
                        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
            }
            None => {
                !segment.is_empty()
                    && segment.bytes().all(|byte| {
                        byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_' || byte == b'.'
                    })
            }
        }
    })
}

/// What one request asked for.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Call {
    /// A GraphQL request, which sends a document.
    Document {
        /// What the sender was doing, from
        /// [`graphql::DOCUMENTS`] when the document is one of
        /// this source's own, and from the caller's own name when it is not.
        name: String,
        /// The document's worst-case node count under the bindings that request sent, or
        /// `None` when the calculation could not rule on the document — which is a defect
        /// in the document rather than a cost of zero.
        node_count: Option<u64>,
    },
    /// A REST request, which sends no document and has no node count.
    Endpoint {
        /// The endpoint it addressed. An [`Endpoint`] rather than a string, so a record
        /// cannot hold the URL a run built — see that type for what it rules on.
        endpoint: Endpoint,
    },
}

impl Call {
    /// What this call is called in a report.
    #[must_use]
    pub fn name(&self) -> &str {
        match self {
            Self::Document { name, .. } => name,
            Self::Endpoint { endpoint } => endpoint.name(),
        }
    }
    /// The budget it draws on.
    #[must_use]
    pub const fn budget(&self) -> Budget {
        match self {
            Self::Document { .. } => Budget::Graphql,
            Self::Endpoint { .. } => Budget::Rest,
        }
    }
    /// The most nodes it may return, for a GraphQL request that has such a number.
    #[must_use]
    pub const fn node_count(&self) -> Option<u64> {
        match self {
            Self::Document { node_count, .. } => *node_count,
            Self::Endpoint { .. } => None,
        }
    }
}

/// One outgoing HTTP request, described before it is sent.
///
/// [`Sending::finished`], and [`Sending::answered`] for the ordinary case, are what turn it
/// into a [`Request`]: a record with no outcome is one nobody could add up, so the outcome
/// is the step that produces the record rather than a field that might be missing.
///
/// Everything a request carries before it is sent is settled by the constructor that made
/// it, which is why there is no builder step here that could attach a reported GraphQL cost
/// to a REST call — a budget metered in requests has no such figure, and GitHub reports
/// none for one.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sending {
    call: Call,
    mode: Mode,
    drawn: Drawn,
}

/// What a request draws on, and what only that kind of request can carry.
///
/// The reported cost lives inside the GraphQL arm rather than beside the call, because a
/// REST request has nowhere to have got one from: GitHub meters that budget in requests and
/// reports no per-call figure at all.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Drawn {
    /// The GraphQL budget, with GitHub's own reported cost for this call when the request
    /// was shaped so GitHub reported one.
    Graphql { reported_cost: Option<u64> },
    /// The REST budget, which a call is its own measure of.
    Rest,
}

impl Sending {
    /// GitHub answered it, with the rate-limit facts its response's headers carried.
    #[must_use]
    pub fn answered(self, limits: RateLimit) -> Request {
        self.finished(Outcome::Answered, limits)
    }
    /// However it ended, with the rate-limit facts its response's headers carried.
    #[must_use]
    pub fn finished(self, outcome: Outcome, limits: RateLimit) -> Request {
        let spend = match (self.drawn, outcome) {
            (_, Outcome::RateLimited) => Spend::not_run(),
            (Drawn::Rest, _) => Spend::counted(),
            (
                Drawn::Graphql {
                    reported_cost: Some(cost),
                },
                _,
            ) => Spend::reported(cost),
            (
                Drawn::Graphql {
                    reported_cost: None,
                },
                _,
            ) => Spend::modelled(),
        };
        Request {
            call: self.call,
            mode: self.mode,
            outcome,
            limits,
            spend,
        }
    }
}

/// What a GraphQL document this crate does not send is called until its sender names it.
const UNNAMED_DOCUMENT: &str = "talking to GitHub";

/// One outgoing HTTP request, and what it cost.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
    call: Call,
    mode: Mode,
    outcome: Outcome,
    limits: RateLimit,
    spend: Spend,
}

impl Request {
    /// Begin recording one GraphQL request carrying `document` under `variables`.
    ///
    /// The name comes from [`graphql::DOCUMENTS`] — the
    /// inventory, not a second list — and the node count from
    /// [`node_count`] under the page sizes `variables` really binds, so
    /// a page read with a caller's smaller limit is counted at that limit rather than at
    /// the worst case. Any page-size variable the request leaves unbound keeps the largest
    /// value this source could send it, which is the worst case for exactly that document.
    ///
    /// `otherwise` names a document the inventory does not hold, which is how a caller's own
    /// calls — a schema introspection, a residue sweep — are named beside this source's;
    /// `None` leaves such a document under the placeholder, and a document the inventory
    /// does hold keeps its entry's name either way.
    ///
    /// `reported_cost` is GitHub's own `cost` for **this** call, from a response to a
    /// request shaped to report one. A `rateLimit(dryRun: true)` probe reports what some
    /// other document would cost and never what this call spent, so its figure does not
    /// belong here.
    #[must_use]
    pub fn graphql(
        document: &str,
        variables: &Value,
        otherwise: Option<&str>,
        reported_cost: Option<u64>,
    ) -> Sending {
        let name = document_name(document);
        Sending {
            call: Call::Document {
                name: match (name, otherwise) {
                    (UNNAMED_DOCUMENT, Some(otherwise)) => otherwise.to_owned(),
                    (name, _) => name.to_owned(),
                },
                // llmlint: ignore[invalid_states_unrepresentable] `None` here is not a missing invariant, it is the honest reading of a document this calculation could not rule on — and the state is reachable, because this constructor is public and `otherwise` exists for a caller's OWN documents (a schema introspection, a residue sweep) which no test of this source's inventory sweeps. Refusing to build the record would lose the request from the accounting entirely, which is strictly worse than recording a call whose node count is unknown. It is never totalled as zero: `Session::total_node_count` filter-maps it out, the report prints the total "over N GraphQL requests that have one", and the by-document section lists only those. Every document this source itself sends is held countable by `tests/node_count.rs`.
                node_count: node_count(document, &bindings(variables)).ok(),
            },
            mode: Mode::of_document(document),
            drawn: Drawn::Graphql { reported_cost },
        }
    }
    /// Begin recording one REST request against `endpoint`.
    ///
    /// `endpoint` is the endpoint rather than the URL that was built from it — `GET
    /// /repos/{owner}/{repo}/labels`, not the repository and label a run happened to name —
    /// because a report is compared between runs and carries no board content. It is an
    /// [`Endpoint`] rather than a string for that reason: what a report may hold is settled
    /// where the endpoint is written, not here.
    #[must_use]
    pub fn rest(endpoint: Endpoint) -> Sending {
        Sending {
            mode: endpoint.method().mode(),
            call: Call::Endpoint { endpoint },
            drawn: Drawn::Rest,
        }
    }
    /// What was called.
    #[must_use]
    pub const fn call(&self) -> &Call {
        &self.call
    }
    /// What it is called in a report.
    #[must_use]
    pub fn name(&self) -> &str {
        self.call.name()
    }
    /// Whether it read or wrote.
    #[must_use]
    pub const fn mode(&self) -> Mode {
        self.mode
    }
    /// How it ended.
    #[must_use]
    pub const fn outcome(&self) -> Outcome {
        self.outcome
    }
    /// The budget it drew on.
    #[must_use]
    pub const fn budget(&self) -> Budget {
        self.call.budget()
    }
    /// The most nodes it may return, for a GraphQL request that has such a number.
    #[must_use]
    pub const fn node_count(&self) -> Option<u64> {
        self.call.node_count()
    }
    /// What it is attributed against its budget, and where that figure came from.
    #[must_use]
    pub const fn spend(&self) -> Spend {
        self.spend
    }
    /// The rate-limit facts this response's own headers carried.
    #[must_use]
    pub const fn rate_limit(&self) -> &RateLimit {
        &self.limits
    }
}

/// What this source calls the document it just sent, from the inventory rather than a copy.
fn document_name(document: &str) -> &str {
    graphql::DOCUMENTS
        .iter()
        .find(|(known, _)| *known == document)
        .map_or(UNNAMED_DOCUMENT, |(_, doing)| *doing)
}

/// The page sizes one request really bound, over the largest this source can send.
///
/// Starting from [`largest_page_sizes`] rather than from nothing is what keeps a document
/// countable when the request leaves one of its page-size variables out: the calculation
/// refuses a `first:` it has no binding for, and the worst case is the honest answer for a
/// size nobody narrowed. A variable no `first:` references — a project number, a page
/// cursor — is ignored by the calculation, so passing every integer through costs nothing.
fn bindings(variables: &Value) -> Variables {
    let mut bindings = largest_page_sizes();
    if let Some(bound) = variables.as_object() {
        for (name, value) in bound {
            if let Some(size) = value.as_u64().and_then(|size| u32::try_from(size).ok()) {
                bindings.insert(name.clone(), size);
            }
        }
    }
    bindings
}

/// Every request one session sent, and what each cost.
///
/// It is on this crate's ordinary code path — [`crate::GitHubProjectsSource`] records into
/// one at the single place a request leaves the crate, with no environment variable, no
/// feature and no build configuration to know about, because an instrument nobody switches
/// on measures nothing. It is constructible and recordable-into from outside the crate for
/// the other half of the same reason: a caller making its own calls beside this source's —
/// the credentialed lane verifying a schema, sweeping residue, cleaning up — accounts for
/// the whole session rather than for this source's share of it.
#[derive(Debug, Default)]
pub struct Accounting {
    requests: Mutex<Vec<Request>>,
    estimates: Mutex<BTreeMap<Budget, u64>>,
}

impl Accounting {
    /// An accounting with nothing in it.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }
    /// Record what a caller estimated this session would spend against `budget`, before
    /// it started.
    ///
    /// An estimate is not an observation and is kept apart from one everywhere below: it is
    /// what a precondition decided on, and the point of carrying it here is that a report
    /// can put it beside what the session really spent, so a reader sees how far the model
    /// was from GitHub's own figures rather than being told to trust it. The last estimate
    /// recorded for a budget is the one reported, because a precondition decides once.
    pub fn estimate(&self, budget: Budget, cost: u64) {
        self.estimates
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(budget, cost);
    }
    /// Record one request.
    pub fn record(&self, request: Request) {
        // A poisoned lock costs the accounting, never the work: a panic elsewhere must not
        // turn measuring the session into a second failure on top of the first.
        self.requests
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(request);
    }
    /// A snapshot of what has been recorded so far: a value to hold and compare, never a
    /// live borrow of this accounting.
    #[must_use]
    pub fn snapshot(&self) -> Session {
        Session {
            requests: self
                .requests
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .clone(),
            estimates: self
                .estimates
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .clone(),
        }
    }
}

/// One session's requests, as a value a caller can hold, compare and report on.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Session {
    requests: Vec<Request>,
    estimates: BTreeMap<Budget, u64>,
}

impl Session {
    /// The requests it holds, in the order they were recorded.
    #[must_use]
    pub fn requests(&self) -> &[Request] {
        &self.requests
    }
    /// How many requests this session sent.
    #[must_use]
    pub fn total_requests(&self) -> usize {
        self.requests.len()
    }
    /// The nodes every GraphQL request that has a node count may return, added up.
    #[must_use]
    pub fn total_node_count(&self) -> u64 {
        self.requests
            .iter()
            .filter_map(Request::node_count)
            .fold(0, u64::saturating_add)
    }
    /// What a precondition estimated this session would spend against `budget`, before it
    /// started, when one estimated anything at all.
    #[must_use]
    pub fn estimated(&self, budget: Budget) -> Option<u64> {
        self.estimates.get(&budget).copied()
    }
    /// What this session is **attributed** against `budget`, summed per call.
    ///
    /// Attributed rather than spent, and the difference is the point: only a call GitHub
    /// itself reported a cost for is a measurement. A call against a budget metered in
    /// requests is its own measure, and every other GraphQL call carries [`Basis::Modelled`]
    /// — one point, GitHub's documented minimum, which is a **lower bound**. So this is what
    /// the session can account for, never a claim about what GitHub charged; [`Self::budgets`]
    /// is where the same total comes apart by basis, and that is what says how much of it is
    /// measured.
    #[must_use]
    pub fn attributed(&self, budget: Budget) -> u64 {
        self.requests
            .iter()
            .filter(|request| request.budget() == budget)
            .fold(0, |total, request| {
                total.saturating_add(request.spend.amount)
            })
    }
    /// Every budget this session touched, in a stable order.
    #[must_use]
    pub fn budgets(&self) -> Vec<BudgetReport> {
        let mut touched: BTreeMap<Budget, BudgetReport> = BTreeMap::new();
        // Seeded from the estimates first: a budget a precondition sized this session
        // against and the session then never reached is worth reporting as exactly that,
        // rather than vanishing from the report that is supposed to compare the two.
        for (budget, estimated) in &self.estimates {
            touched
                .entry(*budget)
                .or_insert_with(|| BudgetReport::of(*budget))
                .estimated = Some(*estimated);
        }
        for request in &self.requests {
            let budget = request.budget();
            touched
                .entry(budget)
                .or_insert_with(|| BudgetReport::of(budget))
                .record(request);
        }
        touched.into_values().collect()
    }
    /// The session report: what a person puts two runs of side by side.
    #[must_use]
    pub fn report(&self) -> String {
        let mut report = String::from("github-projects session accounting\n");
        let count = |mode: Mode| {
            self.requests
                .iter()
                .filter(|request| request.mode == mode)
                .count()
        };
        let ended = |outcome: Outcome| {
            self.requests
                .iter()
                .filter(|request| request.outcome == outcome)
                .count()
        };
        let _ = writeln!(
            report,
            "requests {}: {} {}, {} {}; {} {}, {} {}, {} {}",
            self.total_requests(),
            count(Mode::Read),
            Mode::Read.name(),
            count(Mode::Write),
            Mode::Write.name(),
            ended(Outcome::Answered),
            Outcome::Answered.name(),
            ended(Outcome::Refused),
            Outcome::Refused.name(),
            ended(Outcome::RateLimited),
            Outcome::RateLimited.name(),
        );
        report.push_str("requests by call\n");
        let mut by_call: BTreeMap<&str, (usize, u64, usize)> = BTreeMap::new();
        for request in &self.requests {
            let entry = by_call.entry(request.name()).or_insert((0, 0, 0));
            entry.0 += 1;
            if let Some(nodes) = request.node_count() {
                entry.1 = entry.1.saturating_add(nodes);
                entry.2 += 1;
            }
        }
        for (name, (requests, _, _)) in &by_call {
            let _ = writeln!(report, "  {name:<52}{requests:>6}");
        }
        let counted = self
            .requests
            .iter()
            .filter(|request| request.node_count().is_some())
            .count();
        let _ = writeln!(
            report,
            "node count {} over {counted} GraphQL requests that have one",
            self.total_node_count(),
        );
        report.push_str("node count by document\n");
        for (name, (_, nodes, requests)) in by_call.iter().filter(|(_, (_, _, with))| *with > 0) {
            let counted = format!(
                "{requests} {}",
                if *requests == 1 {
                    "request"
                } else {
                    "requests"
                }
            );
            let _ = writeln!(report, "  {name:<52}{counted:>14}{nodes:>12} nodes");
        }
        for budget in self.budgets() {
            report.push_str(&budget.render());
        }
        report
    }
}

/// One budget a session drew on, with its own figures kept apart from the account's.
///
/// **Every figure here is a total over requests that really drew on this budget, so the only
/// way to have one is to add those requests up.** [`Session::budgets`] is that, and
/// [`BudgetReport::record`] is where a request joins one: the request count, the spend, its
/// three attributions and the account's own readings all move together, from the same record,
/// so a report cannot say it summarises nine requests while its attributions add up to four,
/// or carry a REST budget's figures under [`Budget::Graphql`]. The fields are read through
/// the accessors below for exactly the reason [`RateLimit`]'s are: a hand-assembled set of
/// totals would be a measurement of nothing, and this whole accounting exists because a
/// number nobody measured was argued about instead.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BudgetReport {
    budget: Budget,
    estimated: Option<u64>,
    requests: usize,
    attributed: u64,
    reported: u64,
    modelled: u64,
    counted: u64,
    not_run: usize,
    limit: Option<u64>,
    used_by_the_account: Option<u64>,
    remaining_first_seen: Option<u64>,
    remaining_last_seen: Option<u64>,
}

impl BudgetReport {
    /// A report of `budget` with nothing added to it yet.
    const fn of(budget: Budget) -> Self {
        Self {
            budget,
            estimated: None,
            requests: 0,
            attributed: 0,
            reported: 0,
            modelled: 0,
            counted: 0,
            not_run: 0,
            limit: None,
            used_by_the_account: None,
            remaining_first_seen: None,
            remaining_last_seen: None,
        }
    }
    /// Add one of this budget's requests, and everything its response said about it.
    ///
    /// The one place these totals move, which is what makes them agree with each other and
    /// with the requests they are over.
    fn record(&mut self, request: &Request) {
        self.requests += 1;
        self.attributed = self.attributed.saturating_add(request.spend.amount);
        match request.spend.basis {
            Basis::Reported => {
                self.reported = self.reported.saturating_add(request.spend.amount);
            }
            Basis::Modelled => {
                self.modelled = self.modelled.saturating_add(request.spend.amount);
            }
            Basis::Counted => {
                self.counted = self.counted.saturating_add(request.spend.amount);
            }
            // Attributed nothing, and counted as one of the requests that ran into the
            // limiter. Its headers are still read below — a refusal for a spent budget is
            // the response whose figures say most about that budget's state.
            Basis::NotRun => self.not_run += 1,
        }
        if let Some(limit) = request.limits.limit() {
            self.limit = Some(limit);
        }
        if let Some(used) = request.limits.used_by_the_account() {
            self.used_by_the_account = Some(used);
        }
        if let Some(remaining) = request.limits.remaining() {
            self.remaining_first_seen.get_or_insert(remaining);
            self.remaining_last_seen = Some(remaining);
        }
    }
    /// Which budget.
    #[must_use]
    pub const fn budget(&self) -> Budget {
        self.budget
    }
    /// What a precondition estimated this session would spend against it before it
    /// started, when one estimated anything at all.
    ///
    /// It is an estimate rather than a measurement, and the report says so where it prints
    /// it: what makes it worth carrying is that a reader can see how far the model was from
    /// what the session really spent.
    #[must_use]
    pub const fn estimated(&self) -> Option<u64> {
        self.estimated
    }
    /// How many of this session's requests drew on it.
    #[must_use]
    pub const fn requests(&self) -> usize {
        self.requests
    }
    /// What this session itself is **attributed** against it, summed per call.
    ///
    /// Attributed rather than spent, for the reason [`Session::attributed`] gives: some of
    /// this figure is [`Basis::Modelled`], GitHub's documented one-point minimum, which is a
    /// lower bound and not a measurement. The four figures below are the same total split by
    /// basis, which is what says how much of it GitHub itself reported.
    #[must_use]
    pub const fn attributed(&self) -> u64 {
        self.attributed
    }
    /// How much of that GitHub itself reported.
    #[must_use]
    pub const fn reported(&self) -> u64 {
        self.reported
    }
    /// How much of it is this repository's one-point-per-call lower bound.
    #[must_use]
    pub const fn modelled(&self) -> u64 {
        self.modelled
    }
    /// How much of it is a count of requests against a budget metered in requests.
    #[must_use]
    pub const fn counted(&self) -> u64 {
        self.counted
    }
    /// How many of its requests a rate limiter refused, so they never ran and are
    /// attributed nothing.
    #[must_use]
    pub const fn not_run(&self) -> usize {
        self.not_run
    }
    /// The whole allowance, as GitHub's own headers reported it.
    #[must_use]
    pub const fn limit(&self) -> Option<u64> {
        self.limit
    }
    /// What the **account** had spent, as GitHub's own headers reported it. Not this
    /// session's spend: other work draws on the same budget in the same window.
    #[must_use]
    pub const fn used_by_the_account(&self) -> Option<u64> {
        self.used_by_the_account
    }
    /// The allowance remaining when this session's first request against this budget was
    /// answered.
    #[must_use]
    pub const fn remaining_first_seen(&self) -> Option<u64> {
        self.remaining_first_seen
    }
    /// The allowance remaining when its last one was.
    #[must_use]
    pub const fn remaining_last_seen(&self) -> Option<u64> {
        self.remaining_last_seen
    }
    /// How far the **account's** remaining allowance fell while this session ran.
    ///
    /// A fall rather than a movement, and the difference is not pedantry: an allowance that
    /// *rose* is the hourly window having reset mid-session, which is not a negative spend
    /// and answers zero here. Either way it is not this session's spend, and the report says
    /// so where it prints it — this account is shared, so the difference between two
    /// readings of a shared counter measures the account.
    #[must_use]
    pub fn account_allowance_fall(&self) -> Option<u64> {
        Some(
            self.remaining_first_seen?
                .saturating_sub(self.remaining_last_seen?),
        )
    }
    /// This budget's lines of the session report.
    #[must_use]
    pub fn render(&self) -> String {
        let unknown = |value: Option<u64>| {
            value.map_or_else(|| "not reported".to_owned(), |value| value.to_string())
        };
        let mut lines = format!(
            "budget {}, metered in {}\n",
            self.budget.name(),
            self.budget.unit()
        );
        let _ = writeln!(
            lines,
            "  this session sent {} requests and is attributed {} {} — measured only where \
             GitHub reported it: {} {}, {} {}, {} {}; {} {}",
            self.requests,
            self.attributed,
            self.budget.unit(),
            self.reported,
            Basis::Reported.name(),
            self.modelled,
            Basis::Modelled.name(),
            self.counted,
            Basis::Counted.name(),
            self.not_run,
            Basis::NotRun.name(),
        );
        if let Some(estimated) = self.estimated {
            let _ = writeln!(
                lines,
                "  a precondition estimated {estimated} {} before this session started, \
                 against the {} attributed above",
                self.budget.unit(),
                self.attributed,
            );
        }
        let _ = writeln!(
            lines,
            "  the account: limit {}, {} used, {} remaining when this session finished",
            unknown(self.limit),
            unknown(self.used_by_the_account),
            unknown(self.remaining_last_seen),
        );
        let _ = writeln!(
            lines,
            "  the account's remaining allowance fell {} while this session ran; that is the \
             account's own consumption and not this session's spend, because other work \
             draws on the same budget in the same window",
            unknown(self.account_allowance_fall()),
        );
        lines
    }
}