headwater-check 0.4.0

Generates the rules from the taxonomy, runs them, computes coverage against the census, and keys each instance on what it read and on the clock it was handed
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
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
// SPDX-License-Identifier: Apache-2.0
//! The runner: seventeen checks, coverage against the census, a published read
//! set, a suppression inventory, and text findings in one order.
//!
//! [Spec 12](../../../../docs/spec/12-check-layer.md#two-phases-and-why-the-order-matters)
//! splits a run in two. Phase A classifies and builds, and it is
//! [`headwater_census`] and [`headwater_graph`]. Phase B runs the checks over
//! the graph that Phase A produced, and it is this crate. The order is the
//! answer to the silent pass: the denominator is fixed before any check starts.
//!
//! # Where the rules come from
//!
//! Fifteen of the seventeen are **generated**. None of them names a facet, a
//! kind, a relation, an identifier scheme or a number of days: each reads a
//! declaration out of the resolved taxonomy and instantiates itself over
//! whatever that declaration produced.
//! That is what [spec 12](../../../../docs/spec/12-check-layer.md#the-five-origins-of-a-check)
//! means by "a new facet or relation in the taxonomy produces its checks with
//! no code", and it is why the rule list is short while the instance count is
//! not. [`coverage`] is the runner's own accounting. [`fragment`] and
//! [`duplicate`] read no declaration, because the language has no member that
//! turns prose-link resolution or identifier uniqueness on or off: spec 3
//! states the second of them of every corpus.
//!
//! Four of the five origins are represented. Shape, Graph and Document are
//! here, and Plugin is not. `Corpus` in that table is an *origin* — a rule
//! generated from a declaration that needs many documents — and no such
//! declaration exists yet. [`duplicate`] is corpus-*scoped* and Graph-origin,
//! which is the distinction the two lists have always drawn: the origin is what
//! a rule reads a declaration from, and the grain is what one instance covers.
//!
//! The ten Graph-origin rules span all four grains. [`target`],
//! [`reciprocity`], [`endpoint`], [`dependency`], [`initial_dependency`] and
//! [`basis`] are edge-grained,
//! [`participation`] is neighbourhood-grained, [`duplicate`] is corpus-grained,
//! and [`declaration`] and [`identity`] are **document-grained**. The last two
//! are the ones worth stating: they route the phase-A defects that stop an edge
//! from existing, and an edge-scoped instance exists per edge, so no
//! edge-scoped rule reaches a block whose entries produced none. The origin is
//! what a rule reads and the grain is what one instance covers, and the two
//! were never one statement.
//!
//! The four Document-origin rules are generated from a declaration in the same
//! sense the others are, and two of them meet a limit the language puts there.
//! `voice_regime.forbid` names categories and states no set of them, and
//! `language_regime.controlled` names a language and states no set either. So
//! the engine holds a closed set of each, and an instance that meets a name
//! outside it skips with that name in the reason rather than passing.
//!
//! # What is deliberately absent, and where each piece goes
//!
//! Two parts of the designed check layer are not here, and neither is an
//! oversight. Suppression used to be the third. It is now [`suppression`]: the
//! runner filters findings, records what it filtered, and feeds the inventory
//! into the coverage report, which is where spec 12 puts it.
//!
//! **Change-scoped evaluation, under the name `--changed-only`.** Every
//! instance is created on every run, and the flag does not exist.
//! [#58](https://github.com/headwater-ai/headwater/issues/58) declined to build
//! it and measured why: an instance is keyed on the content hashes of what it
//! read, and [`cache`] serves the ones nothing touched, so a warm run over this
//! repository takes about 57 ms against about 476 ms with no cache
//! ([HW-OBL-0080](../../../../docs/obligations/0080-changed-only-is-the-content-addressed-cache-under-another-name.md)
//! holds the conditions). The cache derives what
//! moved from the bytes rather than from a list a caller supplies, and spec 6
//! forbids a flag that puts an input into a verdict which no reviewer sees.
//! What is left unscoped is Phase A, which no flag reaches.
//!
//! **The generated obligation register.** Every rule below now names the
//! obligation it serves, because the base package declares `obligations` and
//! `controls` and [`register`] is the path from a rule to its obligation. What
//! is absent is the register as an artifact: coverage by obligation,
//! dispositions, and the control health that spec 4 asks a projection to carry.
//! That is [#59](https://github.com/headwater-ai/headwater/issues/59).
//!
//! # Scope is a type, and [`scope`] is where that holds
//!
//! [Spec 12](../../../../docs/spec/12-check-layer.md#the-declaration-is-a-type-not-a-returned-value)
//! rules that a check receives a scoped view and cannot ask for a wider one,
//! and that the enforcement is the feature. Each rule below implements one
//! scope trait, and that trait is the only way to receive the matching view.
//! [`run`] names the seventeen checks it runs, which is the whole of
//! registration.
//! The scope a trait fixes is now read twice: once for the report, and once as
//! a component of the cache key that spec 12 derives from the same fact. The
//! injected clock rides the same declaration, so a rule that reads a date
//! cannot be left out of its own key ([`cache`]).
//!
//! `Neighbourhood` is here, at depth 1, because
//! [`participation`] needed a grain that `Edge` does not reach: an edge-scoped
//! instance exists per edge, and a participation expectation is about an edge
//! that nobody declared. `Corpus` is here for the same kind of reason and one
//! step further out: [`duplicate`] is about two documents that no edge
//! connects, so no relational grain reaches the pair. `Shelf` is still absent,
//! and [`duplicate`] states why it could not have carried this rule either.
//! [`coverage`] is corpus-grained and is still the runner's accounting rather
//! than a check, and it now says which of the two facts about that grain was
//! the reason.
//!
//! # Three phase-A outcomes stay in phase A, and one of them could not
//!
//! Spec 12 calls an unparseable file, an unclassifiable path, a dangling edge
//! and an ambiguous shelf match *structural findings*. Three of the four are a
//! census row. The census accounts for each one with the row that names the
//! author who can act, [`coverage`] reads that census, and a document with no
//! instance is already a finding. So this runner does not re-report them: a
//! second report of one fact sends its author to two places.
//!
//! **A dangling edge is the member with no row, and that made it the member
//! with no rule.** A row is a file and an edge is not one, so nothing in the
//! coverage account reaches it. The graph printed it under its own heading and
//! it answered to no obligation, carried no severity, and left `--strict`
//! exiting 0 over a corpus whose edges pointed at nothing. [`target`] closes
//! that, and #59 supplied what it was waiting for: the base package declares
//! the obligation and the control, so the finding travels the same binding as
//! every other one.
//!
//! The rest of `headwater_graph::Problem` is routed too, and at the other
//! grain. A `relations:` block that is not a mapping, an unknown relation name,
//! an unusable entry, an entry with no `to` and a repeated triple are
//! [`declaration`]. A source document with no identifier is [`identity`], with
//! the two identifier-index defects that say the same thing from the other
//! side. Each of those stops an edge from *existing*, so the unit that survives
//! is the document that wrote the block, and the report the build already
//! produced reaches the check on the view rather than beside it.
//!
//! Every defect of phase A now reaches a rule, and the last one to arrive is
//! the one that needed a fifth grain. Two documents that claim one identifier
//! are `headwater_graph::index::Defect::Duplicate`, a document-scoped instance
//! reads one of the two, and [`duplicate`] reads the corpus. It is the first
//! corpus-scoped check this engine carries, and spec 12 calls those the
//! barriers.

pub mod adoption;
pub mod anchors;
pub mod basis;
pub mod cache;
pub mod change;
pub mod claim;
pub mod command;
pub mod context;
pub mod coverage;
pub mod declaration;
pub mod dependency;
pub mod duplicate;
pub mod endpoint;
pub mod facet_blank;
pub mod facet_required;
pub mod facet_value;
pub mod fill;
pub mod finding;
pub mod fragment;
pub mod frontmatter;
pub mod gate;
pub mod identifier;
pub mod identity;
pub mod initial_dependency;
pub mod instance;
pub mod language;
pub mod lifecycle_state;
pub mod link_path;
pub mod observation;
pub mod outside_root;
pub mod paint;
pub mod participation;
pub mod patch;
pub mod placement;
pub mod promotion;
pub mod readset;
pub mod reciprocity;
pub mod register;
pub mod retention;
pub mod retired;
pub mod scope;
pub mod sections;
pub mod shape;
pub mod source_form;
pub mod state_set_twice;
pub mod suppression;
pub mod surface;
pub mod suspect;
pub mod target;
pub mod transition;
pub mod verification;
pub mod voice;

pub use adoption::Ledger;
#[doc(hidden)]
pub use cache::cached_form;
pub use cache::{rules_digest, Cache};
pub use context::{Context, Date};
pub use coverage::Coverage;
pub use fill::{filled, WIDTH};
pub use finding::{Finding, Severity};
pub use gate::{Recorded, Verdict};
pub use instance::{Input, Instance, Outcome};
pub use observation::{Observation, Observations};
pub use patch::Patch;
pub use readset::{ReadSet, Rule};
pub use register::{Bound, Register};
pub use scope::{
    CorpusCheck, CorpusView, DocumentCheck, DocumentView, EdgeCheck, EdgeUnit, EdgeView, Grain,
    NeighbourhoodCheck, NeighbourhoodView, Scope,
};
pub use shape::{Purpose, Shape};
pub use suppression::Inventory;

use headwater_census::census::Census;
use headwater_census::shelves::Taxonomy;
use headwater_graph::{Declarations, Graph};

/// The rules this runner carries, in the order a report lists them.
///
/// Twenty-six are generated from the taxonomy, three read no declaration, one
/// is the coverage guarantee itself, and the last three are about the taxonomy
/// rather than about the corpus. A rule that is generated has no entry of its
/// own anywhere: the list is the *templates*, and the instance count is what a
/// taxonomy decides.
///
/// The order is the five origins of
/// [spec 12](../../../../docs/spec/12-check-layer.md#the-five-origins-of-a-check),
/// which is Shape, then Graph, then the runner's own accounting.
pub const RULES: [&str; 39] = [
    facet_required::RULE,
    facet_value::RULE,
    facet_blank::RULE,
    identifier::RULE,
    placement::RULE,
    target::RULE,
    suspect::RULE,
    reciprocity::RULE,
    endpoint::RULE,
    dependency::RULE,
    initial_dependency::RULE,
    basis::RULE,
    participation::RULE,
    state_set_twice::RULE,
    declaration::RULE,
    identity::RULE,
    duplicate::RULE,
    claim::MISSING,
    claim::STALE,
    voice::RULE,
    language::RULE,
    retired::RULE,
    surface::RULE,
    command::RULE,
    source_form::RULE,
    sections::RULE,
    fragment::RULE,
    link_path::RULE,
    promotion::RULE,
    transition::RULE,
    lifecycle_state::RULE,
    retention::RULE,
    coverage::RULE,
    register::DISPOSITION,
    register::MECHANISM,
    register::OBSERVATION,
    adoption::RULE,
    outside_root::RULE,
    verification::RULE,
];

/// The declarations one run reads, from a taxonomy that is already resolved.
///
/// Four readers, each with the list of fields its own phase needs, and this is
/// where they arrive together. See [`shape`] for why widening one reader was
/// the alternative and what it would have cost.
pub struct Declared<'a> {
    /// The digest of the lock these four came out of.
    ///
    /// It is here rather than on [`Context`] because it is a fact about the
    /// taxonomy and not an injected value, and it is here rather than nowhere
    /// because [`ReadSet`] has to carry it: a lock that moved voids every
    /// result at once, and a gate reading a read set with no lock in it would
    /// decide that a verdict survived a taxonomy change.
    pub lock: &'a str,
    /// Shelves and abstract kinds, which is what kind resolution read.
    pub taxonomy: &'a Taxonomy,
    /// Facets, the kind hierarchy, and the participation expectations.
    pub shape: &'a Shape,
    /// Relation types and anchor kinds.
    pub relations: &'a Declarations,
    /// The two front-matter keys the graph phase reads by name, and the one
    /// thing here that is not a declaration.
    ///
    /// It is here because a rule that reads an identifier has to read it from
    /// somewhere, and no declaration states where. A kind declares
    /// `identifier: {scheme: …}` and nothing names the key that holds the
    /// minted value, so [`headwater_graph::Config`] carries the guess and
    /// `.headwater/README.md` records it. The alternative was to write `id`
    /// into [`identifier`], which would put the same guess in two places and
    /// let them disagree. Taking the parameter means that settling the question
    /// changes a declaration, and that a corpus whose identifiers live under
    /// another key gets one answer from the index and the same answer from the
    /// rule.
    pub config: &'a headwater_graph::Config,
    /// Obligations and controls: the path from a rule to what it serves.
    pub register: &'a Register,
    /// The committed snapshot of which control naming a mechanism outside
    /// this engine has been seen to run, and at what commit.
    ///
    /// A fact about the corpus rather than about the taxonomy, so it is read
    /// off the tree beside it rather than out of the resolved lock, on the
    /// terms [`crate::claim::Claims`] already reads `.headwater/ids/` by: see
    /// [`observation`].
    pub observations: &'a Observations,
    /// The `adoption` block of the lock, where the lock declares one.
    ///
    /// It arrives as a mapping rather than as tasks because the lock does not
    /// know what a rule is. [`crate::adoption::read`] turns it into tasks and
    /// reports what it could not read.
    pub adoption: Option<&'a headwater_yaml::Mapping>,
    /// Where the declarations above came from, as a path a reader can open.
    ///
    /// It is here because two rules of [`register`] are about the taxonomy
    /// rather than about the corpus, and a finding carries a path. For a run of
    /// the verb that is `.headwater/taxonomy.lock`, which spec 6 fixes as the
    /// one thing downstream reads.
    pub source: &'a str,
}

/// One run of the check layer over one corpus.
#[derive(Clone, Debug)]
pub struct Run {
    /// Every instance of every check, in the order the checks are listed.
    pub instances: Vec<Instance>,
    pub coverage: Coverage,
    /// Every finding a reader sees, in the one order
    /// [spec 12](../../../../docs/spec/12-check-layer.md#determinism-concretely)
    /// fixes. A finding an author suppressed is not here, and it is in
    /// [`Run::suppressions`] instead.
    pub findings: Vec<Finding>,
    /// What this run's authors suppressed, and what became of each directive.
    /// See [`suppression`]: the filter is the runner's, and a check never sees
    /// it.
    pub adoption: Ledger,
    pub suppressions: Inventory,
    /// What each rule sees and what it serves, in [`RULES`] order. A rule that
    /// reaches no obligation is in this list too, because a rule that cannot
    /// say which invariant it protects is what spec 4 asks a reader to notice.
    pub served: Vec<Serves>,
    /// The union of what this run read, with the lock, the clock and the check
    /// versions beside it. See [`readset`].
    pub read_set: ReadSet,
    /// The register: every obligation with its disposition, every control with
    /// its health, and what escaped under each obligation. Spec 4 makes it a
    /// projection of the two declarations, generated and never authored.
    pub register: register::Projection,
    /// Every verification of the corpus with its state: `declared`,
    /// `observed at <commit>` or `suspect since <commit>`. A projection that
    /// no cache stores, beside the register. See [`verification::Block`].
    pub verifications: verification::Block,
    /// The change this run was scoped to, and nothing for a full-corpus run.
    ///
    /// It states an input rather than a verdict, which is why it is here beside
    /// the read set: a reader who cannot see what a run was scoped to cannot
    /// reproduce it from what it printed. See [`Scoped`].
    pub change: Option<Scoped>,
    /// What this run did with its cache. Deliberately outside [`Run::render`]:
    /// see [`cache`] for why a hit count is not part of a verdict.
    pub cache: cache::Report,
}

/// One rule, the scope that binds it, and the obligation it serves.
#[derive(Clone, Debug)]
pub struct Serves {
    pub rule: &'static str,
    /// Derived from the trait the check implements, and never stated beside
    /// it. See [`scope`] for why that distinction is the whole feature.
    pub scope: Scope,
    /// Which edition of the rule ran. Read off the same trait as the scope,
    /// and for the same reason: it is a component of every key this run wrote,
    /// so a read set that stated a different one would describe another run.
    pub version: u32,
    pub obligation: Bound,
    /// The emitter targets this rule exports to, read off the same trait as
    /// the scope and the edition. See [`scope::ExportTargets`], and
    /// [`partition`] for the rule that keeps the claim honest.
    pub exportable_as: scope::ExportTargets,
}

/// The check registry: every rule, with the scope, the edition and the export
/// targets that its declaration carries.
///
/// One list, read in three places. A rule that appears here and not in
/// [`RULES`] is a compile error, because the array is sized from it.
fn registry() -> [(&'static str, Scope, u32, scope::ExportTargets); RULES.len()] {
    [
        (
            facet_required::RULE,
            scope::document_scope::<facet_required::Required>(),
            scope::document_version::<facet_required::Required>(),
            scope::document_exports::<facet_required::Required>(),
        ),
        (
            facet_value::RULE,
            scope::document_scope::<facet_value::Values>(),
            scope::document_version::<facet_value::Values>(),
            scope::document_exports::<facet_value::Values>(),
        ),
        (
            facet_blank::RULE,
            scope::document_scope::<facet_blank::Blank>(),
            scope::document_version::<facet_blank::Blank>(),
            scope::document_exports::<facet_blank::Blank>(),
        ),
        (
            identifier::RULE,
            scope::document_scope::<identifier::Identifier>(),
            scope::document_version::<identifier::Identifier>(),
            scope::document_exports::<identifier::Identifier>(),
        ),
        (
            placement::RULE,
            scope::document_scope::<placement::Placement>(),
            scope::document_version::<placement::Placement>(),
            scope::document_exports::<placement::Placement>(),
        ),
        (
            target::RULE,
            scope::edge_scope::<target::Targets<'_>>(),
            scope::edge_version::<target::Targets<'_>>(),
            scope::edge_exports::<target::Targets<'_>>(),
        ),
        (
            suspect::RULE,
            scope::edge_scope::<suspect::Suspect<'_>>(),
            scope::edge_version::<suspect::Suspect<'_>>(),
            scope::edge_exports::<suspect::Suspect<'_>>(),
        ),
        (
            reciprocity::RULE,
            scope::edge_scope::<reciprocity::Reciprocity>(),
            scope::edge_version::<reciprocity::Reciprocity>(),
            scope::edge_exports::<reciprocity::Reciprocity>(),
        ),
        (
            endpoint::RULE,
            scope::edge_scope::<endpoint::Endpoints<'_>>(),
            scope::edge_version::<endpoint::Endpoints<'_>>(),
            scope::edge_exports::<endpoint::Endpoints<'_>>(),
        ),
        (
            dependency::RULE,
            scope::edge_scope::<dependency::Dependency<'_>>(),
            scope::edge_version::<dependency::Dependency<'_>>(),
            scope::edge_exports::<dependency::Dependency<'_>>(),
        ),
        (
            initial_dependency::RULE,
            scope::edge_scope::<initial_dependency::InitialDependency<'_>>(),
            scope::edge_version::<initial_dependency::InitialDependency<'_>>(),
            scope::edge_exports::<initial_dependency::InitialDependency<'_>>(),
        ),
        (
            basis::RULE,
            scope::edge_scope::<basis::Basis<'_>>(),
            scope::edge_version::<basis::Basis<'_>>(),
            scope::edge_exports::<basis::Basis<'_>>(),
        ),
        (
            participation::RULE,
            scope::neighbourhood_scope::<participation::Participation<'_>>(),
            scope::neighbourhood_version::<participation::Participation<'_>>(),
            scope::neighbourhood_exports::<participation::Participation<'_>>(),
        ),
        (
            state_set_twice::RULE,
            scope::corpus_scope::<state_set_twice::StateSetTwice<'_>>(),
            scope::corpus_version::<state_set_twice::StateSetTwice<'_>>(),
            scope::corpus_exports::<state_set_twice::StateSetTwice<'_>>(),
        ),
        (
            declaration::RULE,
            scope::document_scope::<declaration::Unusable<'_>>(),
            scope::document_version::<declaration::Unusable<'_>>(),
            scope::document_exports::<declaration::Unusable<'_>>(),
        ),
        (
            identity::RULE,
            scope::document_scope::<identity::Identity<'_>>(),
            scope::document_version::<identity::Identity<'_>>(),
            scope::document_exports::<identity::Identity<'_>>(),
        ),
        (
            duplicate::RULE,
            scope::corpus_scope::<duplicate::Duplicate>(),
            scope::corpus_version::<duplicate::Duplicate>(),
            scope::corpus_exports::<duplicate::Duplicate>(),
        ),
        (
            claim::MISSING,
            scope::corpus_scope::<claim::Missing<'_>>(),
            scope::corpus_version::<claim::Missing<'_>>(),
            scope::corpus_exports::<claim::Missing<'_>>(),
        ),
        (
            claim::STALE,
            scope::corpus_scope::<claim::Stale<'_>>(),
            scope::corpus_version::<claim::Stale<'_>>(),
            scope::corpus_exports::<claim::Stale<'_>>(),
        ),
        (
            voice::RULE,
            scope::document_scope::<voice::Voice>(),
            scope::document_version::<voice::Voice>(),
            scope::document_exports::<voice::Voice>(),
        ),
        (
            language::RULE,
            scope::document_scope::<language::Language>(),
            scope::document_version::<language::Language>(),
            scope::document_exports::<language::Language>(),
        ),
        (
            retired::RULE,
            scope::document_scope::<retired::Retired>(),
            scope::document_version::<retired::Retired>(),
            scope::document_exports::<retired::Retired>(),
        ),
        (
            surface::RULE,
            scope::document_scope::<surface::LocalPath>(),
            scope::document_version::<surface::LocalPath>(),
            scope::document_exports::<surface::LocalPath>(),
        ),
        (
            command::RULE,
            scope::document_scope::<command::Undeclared>(),
            scope::document_version::<command::Undeclared>(),
            scope::document_exports::<command::Undeclared>(),
        ),
        (
            source_form::RULE,
            scope::document_scope::<source_form::SourceForm>(),
            scope::document_version::<source_form::SourceForm>(),
            scope::document_exports::<source_form::SourceForm>(),
        ),
        (
            sections::RULE,
            scope::document_scope::<sections::Sections>(),
            scope::document_version::<sections::Sections>(),
            scope::document_exports::<sections::Sections>(),
        ),
        (
            fragment::RULE,
            scope::corpus_scope::<fragment::Fragments>(),
            scope::corpus_version::<fragment::Fragments>(),
            scope::corpus_exports::<fragment::Fragments>(),
        ),
        (
            link_path::RULE,
            scope::corpus_scope::<link_path::Paths>(),
            scope::corpus_version::<link_path::Paths>(),
            scope::corpus_exports::<link_path::Paths>(),
        ),
        (
            promotion::RULE,
            scope::document_scope::<promotion::Promoted>(),
            scope::document_version::<promotion::Promoted>(),
            scope::document_exports::<promotion::Promoted>(),
        ),
        (
            transition::RULE,
            scope::document_scope::<transition::Transition<'_>>(),
            scope::document_version::<transition::Transition<'_>>(),
            scope::document_exports::<transition::Transition<'_>>(),
        ),
        (
            lifecycle_state::RULE,
            scope::document_scope::<lifecycle_state::StateAdmitted<'_>>(),
            scope::document_version::<lifecycle_state::StateAdmitted<'_>>(),
            scope::document_exports::<lifecycle_state::StateAdmitted<'_>>(),
        ),
        (
            retention::RULE,
            scope::corpus_scope::<retention::Retention<'_>>(),
            scope::corpus_version::<retention::Retention<'_>>(),
            scope::corpus_exports::<retention::Retention<'_>>(),
        ),
        (
            coverage::RULE,
            coverage::SCOPE,
            coverage::VERSION,
            coverage::EXPORTABLE_AS,
        ),
        (
            register::DISPOSITION,
            register::SCOPE,
            register::VERSION,
            register::EXPORTABLE_AS,
        ),
        (
            register::MECHANISM,
            register::SCOPE,
            register::VERSION,
            register::EXPORTABLE_AS,
        ),
        (
            register::OBSERVATION,
            register::SCOPE,
            register::VERSION,
            register::EXPORTABLE_AS,
        ),
        (
            adoption::RULE,
            adoption::SCOPE,
            adoption::VERSION,
            adoption::EXPORTABLE_AS,
        ),
        (
            outside_root::RULE,
            outside_root::SCOPE,
            outside_root::VERSION,
            outside_root::EXPORTABLE_AS,
        ),
        (
            verification::RULE,
            scope::edge_scope::<verification::Verified<'_>>(),
            scope::edge_version::<verification::Verified<'_>>(),
            scope::edge_exports::<verification::Verified<'_>>(),
        ),
    ]
}

/// The check registry, split in two for one emitter target.
///
/// [Spec 12](../../../../docs/spec/12-check-layer.md#exportable_as-is-a-set-with-a-partition-rule)
/// asks for both halves and for neither to be authored. Both come from
/// [`declared`], so no rule can fall into both or into neither, and a rule
/// added to the registry lands in one of them without anybody remembering to
/// put it there.
#[derive(Clone, Debug)]
pub struct Partition {
    pub target: String,
    pub exported: Vec<&'static str>,
    pub unexported: Vec<&'static str>,
}

/// Split the registry for `target`, in [`RULES`] order.
pub fn partition(target: &str) -> Partition {
    let mut exported = Vec::new();
    let mut unexported = Vec::new();
    for (rule, _, _, targets) in registry() {
        match targets.contains(&target) {
            true => exported.push(rule),
            false => unexported.push(rule),
        }
    }
    Partition {
        target: target.to_string(),
        exported,
        unexported,
    }
}

/// Run every check over one census and the graph built from it.
///
/// The census and the graph are arguments rather than something this function
/// builds, for the reason the graph build takes a census: two passes over one
/// corpus can disagree, and the pair that would disagree here is the
/// denominator and the thing measured against it.
pub fn run(
    census: &Census,
    graph: &Graph,
    declared: &Declared<'_>,
    claims: &claim::Claims,
    ctx: &Context,
    cache: &mut Cache,
) -> Run {
    // Registration, in full: twenty-six checks, each named once. The scope trait each
    // one implements decides what it is handed, so this function cannot widen
    // a view by calling the wrong instantiation.
    let required = facet_required::Required::over(declared.shape);
    let values = facet_value::Values::over(declared.shape);
    // The state between the two rules above: the key is declared, and it
    // carries no content. See [`facet_blank`].
    let blank = facet_blank::Blank::over(declared.shape);
    let identifiers =
        identifier::Identifier::over(declared.shape, &declared.config.identifier_facet);
    let placement = placement::Placement::over(declared.taxonomy);
    let targets = target::Targets::over(declared.relations);
    // The drift rule, over the relations that can carry a revision: the ones
    // an importer may write, and the ones onto an anchor kind. See [`suspect`].
    let suspect = suspect::Suspect::over(declared.relations, declared.shape);
    // A lone half written by a document at an initial state is owed nothing
    // yet. See [`reciprocity`].
    let reciprocity = reciprocity::Reciprocity::over(declared.relations, declared.shape);
    let endpoints = endpoint::Endpoints::over(declared.relations, declared.shape);
    // A live document resting on a terminal one, over the relations whose
    // family a core requirement declares `lifecycle_sensitive`. Edge-scoped
    // because the unit is the pair: one endpoint decides nothing here. See
    // [`dependency`].
    let dependency = dependency::Dependency::over(declared.relations, declared.shape);
    // A live document resting on a draft one, over every relation except one
    // that writes a state onto its target. See [`initial_dependency`].
    let initial_dependency =
        initial_dependency::InitialDependency::over(declared.relations, declared.shape);
    // An evidenced claim resting on a document nobody read, over the relations
    // whose declared family is `evidence`. Edge-scoped because the unit is the
    // pair: the claim is at one end and the warrant is at the other. See
    // [`basis`].
    let basis = basis::Basis::over(declared.relations);
    // A verification's freshness against the criterion it proves, over the
    // relations that reach a `verification` kind, and against the committed
    // observation snapshot. Edge-scoped for [`basis`]'s own reason: the
    // criterion is at one end and the verification's snapshot entry names the
    // other. See [`verification`].
    let verified = verification::Verified::over(declared.relations, declared.observations);
    let participation = participation::Participation::over(declared.shape, declared.relations);
    // Two relations telling one generated document two states, over the
    // relations that declare `on_target.set_state`. See [`state_set_twice`].
    let set_twice = state_set_twice::StateSetTwice::over(declared.relations, declared.shape);
    let declarations = declaration::Unusable::over(declared.relations, declared.shape);
    let identities = identity::Identity::over(
        declared.relations,
        declared.shape,
        &declared.config.identifier_facet,
    );
    let duplicates = duplicate::Duplicate::over(&declared.config.identifier_facet);
    // The two rules that hold the identifier claim store. They read the index
    // the build already made rather than the corpus a second time, and the
    // store reaches them through the view, which is what puts it in the key.
    // See [`claim`].
    let claim_missing = claim::Missing::over(declared.shape, declared.taxonomy, &graph.index);
    let claim_stale = claim::Stale::over(
        &declared.config.identifier_facet,
        declared.shape,
        &graph.index,
    );
    let voice = voice::Voice::over(declared.shape);
    let language = language::Language::over(declared.shape);
    let retired = retired::Retired::over(declared.shape);
    let surface = surface::LocalPath::over(declared.shape);
    let commands = command::Undeclared::over(declared.shape);
    let source_form = source_form::SourceForm::over(declared.shape);
    let sections = sections::Sections::over(declared.shape);
    // Whether a fragment names a heading of the document it points at, whether
    // that is the citing document or another one. Corpus-scoped for
    // [`link_path`]'s reason below, and because handing a document-scoped rule
    // the target documents would count each of them as checked by this rule.
    // See [`fragment`].
    let fragments = fragment::Fragments;
    // The path half of the same question, and it reads nothing of its own
    // either: the set of links that resolved to nothing is what the build
    // already produced and the report already prints. Corpus-scoped, because a
    // link is dead when no file stands at its path and that is not a fact about
    // the file that wrote it. See [`link_path`].
    let link_paths = link_path::Paths;
    // The one rule that declares `NEEDS_PRIOR`, and it carries no declaration:
    // the transition it reads is spec 3's act rather than a member of any
    // taxonomy. See [`promotion`].
    let promoted = promotion::Promoted;
    // The second, and this one carries a declaration: the machine it reads is
    // `regimes.lifecycle` of the resolved taxonomy. See [`transition`].
    let transitions = transition::Transition::over(declared.shape);
    // The state a document stands in, against the states the regime of its kind
    // names. Document-scoped rather than change-scoped, because the defect
    // survives in the document and a document authored at a wrong state made no
    // movement to read. See [`lifecycle_state`].
    let standing = lifecycle_state::StateAdmitted::over(declared.shape);
    // The third, and the only one about a document this corpus no longer
    // holds. Corpus-scoped because a deleted document has no census row to
    // instantiate over, and it declares the corpus-grained half of the prior
    // input. See [`retention`].
    let retention = retention::Retention::over(declared.taxonomy, declared.shape);

    let digests = scope::Digests::of(census);
    let mut instances = scope::over_documents(&required, census, graph, ctx, cache);
    instances.extend(scope::over_documents(&values, census, graph, ctx, cache));
    instances.extend(scope::over_documents(&blank, census, graph, ctx, cache));
    instances.extend(scope::over_documents(
        &identifiers,
        census,
        graph,
        ctx,
        cache,
    ));
    instances.extend(scope::over_documents(&placement, census, graph, ctx, cache));
    instances.extend(scope::over_edges(
        &targets,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &suspect,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &reciprocity,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &endpoints,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &dependency,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &initial_dependency,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &basis,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_edges(
        &verified,
        census,
        graph,
        &digests,
        declared.observations,
        ctx,
        cache,
    ));
    instances.extend(scope::over_neighbourhoods(
        &participation,
        census,
        graph,
        &digests,
        ctx,
        cache,
    ));
    // The one corpus-scoped rule with a generation step. A taxonomy whose
    // setter relations all name one state can tell no document two, so the
    // instance is not built. See [`state_set_twice`].
    if set_twice.can_clash() {
        instances.extend(scope::over_corpus(
            &set_twice, census, graph, claims, ctx, cache,
        ));
    }
    instances.extend(scope::over_documents(
        &declarations,
        census,
        graph,
        ctx,
        cache,
    ));
    instances.extend(scope::over_documents(
        &identities,
        census,
        graph,
        ctx,
        cache,
    ));
    instances.extend(scope::over_corpus(
        &duplicates,
        census,
        graph,
        claims,
        ctx,
        cache,
    ));
    instances.extend(scope::over_corpus(
        &claim_missing,
        census,
        graph,
        claims,
        ctx,
        cache,
    ));
    instances.extend(scope::over_corpus(
        &claim_stale,
        census,
        graph,
        claims,
        ctx,
        cache,
    ));
    instances.extend(scope::over_documents(&voice, census, graph, ctx, cache));
    instances.extend(scope::over_documents(&language, census, graph, ctx, cache));
    instances.extend(scope::over_documents(&retired, census, graph, ctx, cache));
    instances.extend(scope::over_documents(&surface, census, graph, ctx, cache));
    instances.extend(scope::over_documents(&commands, census, graph, ctx, cache));
    instances.extend(scope::over_documents(
        &source_form,
        census,
        graph,
        ctx,
        cache,
    ));
    // The paths a language regime lists outside the corpus root, and the three
    // rules HW-DR-0084 clause 5 gives them. No other registration here reaches
    // one: `over_outside_root` takes only a check that implements
    // `OutsideCheck`, and these three are the only ones that do.
    instances.extend(scope::over_outside_root(&language, census, cache));
    instances.extend(scope::over_outside_root(&retired, census, cache));
    instances.extend(scope::over_outside_root(&source_form, census, cache));
    instances.extend(scope::over_documents(&sections, census, graph, ctx, cache));
    instances.extend(scope::over_corpus(
        &fragments, census, graph, claims, ctx, cache,
    ));
    instances.extend(scope::over_corpus(
        &link_paths,
        census,
        graph,
        claims,
        ctx,
        cache,
    ));
    instances.extend(scope::over_documents(&promoted, census, graph, ctx, cache));
    instances.extend(scope::over_documents(
        &transitions,
        census,
        graph,
        ctx,
        cache,
    ));
    instances.extend(scope::over_documents(&standing, census, graph, ctx, cache));
    instances.extend(scope::over_corpus(
        &retention, census, graph, claims, ctx, cache,
    ));

    let coverage = Coverage::of(census, &instances);

    let mut register = register::Projection::of(declared.register, declared.observations);

    // Read here, ahead of the findings list it feeds, rather than beside
    // `adoption::apply` below. `adoption::expired` needs it to contribute
    // findings of its own into the same list the register's two do, and that
    // list is stamped with an obligation and sorted before `apply` ever sees
    // it, so a finding `expired` invents there would never be stamped.
    let declared_payload = match declared.adoption {
        Some(block) => adoption::read(block, &RULES),
        None => adoption::Declared::default(),
    };

    let mut findings: Vec<Finding> = instances
        .iter()
        .flat_map(|instance| instance.findings().iter().cloned())
        .collect();
    findings.extend(coverage.findings());
    // The register's two findings and adoption's one are about the taxonomy
    // rather than about the corpus, and they enter here for the reason
    // coverage's does: none of the three rules creates an instance, so none
    // accounts anything against the census.
    findings.extend(register.findings(declared.source));
    findings.extend(adoption::expired(
        &declared_payload,
        declared.source,
        ctx.now(),
    ));
    // A path a language regime lists outside the corpus root that reads
    // nothing, as an error, for the reason adoption's enters here: it is about
    // the taxonomy read against the tree, and it creates no instance.
    findings.extend(outside_root::findings(census, declared.source));

    // The obligation is stamped here rather than written into each rule,
    // because the binding is data. A rule states its id, a control names that
    // id and the obligations it discharges, and one place reads the two
    // together. See [`register`] for why that place is not the check.
    let served: Vec<Serves> = registry()
        .into_iter()
        .map(|(rule, scope, version, exportable_as)| Serves {
            rule,
            scope,
            version,
            obligation: declared.register.bound(rule),
            exportable_as,
        })
        .collect();
    for finding in &mut findings {
        finding.obligation = match served.iter().find(|served| served.rule == finding.rule) {
            Some(Serves {
                obligation: Bound::To(obligation),
                ..
            }) => Some(obligation.clone()),
            _ => None,
        };
    }

    // The filter is here, after every instance has an outcome and after the
    // obligation is stamped. So a cache holds what a check decided, an
    // inventory holds what a reader did not see, and the two cannot drift
    // ([`suppression`]).
    // The adoption payload runs first, which is the precedence spec 4 fixes:
    // waiver, then migration-pending, then suppression. A finding a task holds
    // never reaches a directive, so the two inventories partition by the order
    // of these two calls rather than by a rule checked afterwards.
    // `declared_payload` was read above, ahead of the findings list, so that
    // `adoption::expired` could contribute to it. It moves in by value here,
    // unchanged in shape.
    let (findings, adoption) =
        adoption::apply(finding::sorted(findings), declared_payload, ctx.now());

    let (declared_suppressions, refused) = suppression::declared(census, &RULES);
    let (findings, suppressions) =
        suppression::apply(findings, declared_suppressions, refused, ctx.now());

    // After both filters, because what escaped is what a reader of the findings
    // list did not see there, and the two inventories are where that is
    // recorded. They are counted apart so that the partition is checkable.
    register.escaped_from(declared.register, &suppressions);
    register.pending_from(declared.register, &adoption);

    // Every rule as this run served it, which is where a read set takes the
    // edition and the clock declaration from. Both come off the trait, so
    // neither is a second fact beside the scope ([`scope`]).
    let rules: Vec<Rule> = served
        .iter()
        .map(|served| Rule {
            name: served.rule,
            version: served.version,
            needs_clock: served.scope.needs_clock(),
            needs_prior: served.scope.needs_prior(),
        })
        .collect();
    let mut read_set = ReadSet::of(declared.lock, ctx.now(), &rules, &instances);
    // The observation snapshot belongs to no instance, so the union above
    // never sees it: `register::Projection::of` reads it directly, at
    // `Grain::Taxonomy`, outside every per-document `reads` list. Added here
    // on the same terms `crate::claim::STORE` is, so `headwater gate` catches
    // an edit to it between the tree this run read and the tree a later gate
    // reads: see `observation::Observations::read_set_digest`.
    if let Some(digest) = declared.observations.read_set_digest() {
        if let Err(at) = read_set
            .inputs
            .binary_search_by(|known| known.path.as_str().cmp(observation::PATH))
        {
            read_set
                .inputs
                .insert(at, Input::new(observation::PATH, digest));
        }
    }

    // What the change carried, and what the one rule that reads it made of it.
    // The promotion count is derived from the findings rather than counted
    // beside them, so the line and the findings under it cannot disagree.
    let change = ctx.change().map(|change| Scoped {
        named: change.named(),
        unmatched: change.unmatched().into_iter().map(str::to_string).collect(),
        promotions: findings
            .iter()
            .filter(|finding| finding.rule == promotion::RULE)
            .count(),
    });

    Run {
        instances,
        coverage,
        findings,
        adoption,
        suppressions,
        served,
        read_set,
        register,
        verifications: verification::Block::of(
            declared.relations,
            census,
            graph,
            declared.observations,
        ),
        change,
        cache: cache.report(),
    }
}

/// What one run's change carried, for the report that states its own inputs.
///
/// A full-corpus run has none of this, and the absence is the statement: spec 12
/// makes the prior version available only in change-scoped evaluation, and
/// coverage already reports every instance that skipped for want of one.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Scoped {
    /// The documents the change named, and what became of each.
    pub named: change::Named,
    /// The paths the change named that this corpus holds no row at, in path
    /// order. Nothing is checked over one, so the report is the only place one
    /// is ever seen.
    pub unmatched: Vec<String>,
    /// Warrants that moved from `asserted` to `accepted` in this change.
    ///
    /// The count [spec 3](../../../../docs/spec/03-authoring-and-lifecycle.md#promotion-is-one-human-one-document-one-diff)
    /// asks for, and the one reading that a standing population cannot give: a
    /// bulk stamp lowers the `asserted` count exactly as the same number of
    /// real acceptances would, and it raises this one all at once.
    pub promotions: usize,
}

impl Scoped {
    /// The block a report opens with when a run was scoped to a change.
    pub fn render(&self) -> String {
        use std::fmt::Write;
        let mut out = String::new();
        let _ = writeln!(
            out,
            "scoped to a change: {} documents named, {} added, {} with a prior version this run \
             read",
            self.named.documents, self.named.added, self.named.carried
        );
        if self.named.unreadable > 0 {
            let _ = writeln!(
                out,
                "  {:5} prior versions did not read, and every instance over one is skipped with \
                 its reason rather than passed",
                self.named.unreadable
            );
        }
        // Above the count, because it is the line that says the count is over
        // fewer documents than the caller named. A path that reached no row is
        // checked by nothing, so no skipped instance carries it and this is the
        // only report of one.
        if !self.unmatched.is_empty() {
            let _ = writeln!(
                out,
                "  {:5} named no row of this corpus, so nothing was checked over them:",
                self.unmatched.len()
            );
            for path in &self.unmatched {
                let _ = writeln!(out, "        {path}");
            }
        }
        let _ = writeln!(
            out,
            "  {:5} promoted from `{}` to `{}`. Nothing declares how many promotions in one \
             change is too many",
            self.promotions,
            promotion::FROM,
            promotion::TO
        );
        out
    }
}

/// How much of a run to write out.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Detail {
    /// Every instance, whatever it found. Right for a fixture tree, where each
    /// one is the point.
    EveryInstance,
    /// The totals, and every finding.
    Findings,
    /// The totals alone: coverage, the instances each rule created, and what
    /// each rule serves. No finding, and no count of findings.
    ///
    /// Right for a recorded run over a corpus of *prose*, and wrong for a
    /// fixture tree. A finding of a Document-origin rule is a function of a
    /// sentence, and a sentence changes on most commits: a file that records
    /// them is a file that is re-blessed rather than read. What a regression
    /// moves is above this line — the denominator, the instance count per rule,
    /// and the obligation each rule reaches — and none of that moves when an
    /// author rewrites a paragraph.
    Totals,
}

impl Run {
    /// Whether a `--strict` invocation would fail on this run.
    pub fn has_errors(&self) -> bool {
        self.findings
            .iter()
            .any(|finding| finding.severity == Severity::Error)
    }

    /// Findings per severity, in severity order, so that two reports line up.
    pub fn counts(&self) -> Vec<(Severity, usize)> {
        [Severity::Error, Severity::Warn, Severity::Info]
            .into_iter()
            .map(|severity| {
                (
                    severity,
                    self.findings
                        .iter()
                        .filter(|finding| finding.severity == severity)
                        .count(),
                )
            })
            .collect()
    }

    /// Instances per rule, in the order [`RULES`] lists them.
    pub fn per_rule(&self) -> Vec<(&'static str, usize)> {
        RULES
            .iter()
            .map(|rule| {
                (
                    *rule,
                    self.instances
                        .iter()
                        .filter(|instance| instance.rule == *rule)
                        .count(),
                )
            })
            .collect()
    }

    /// The run as text.
    ///
    /// Coverage comes first and it accounts for every file, whatever `detail`
    /// then prints. Same order and same argument as the census and the graph: a
    /// reader who checks nothing else still sees the denominator, and "no
    /// findings across 36 of 36" and "no findings across 30 of 36" are not the
    /// same result.
    ///
    /// `mode` is the color decision the caller already made, threaded down to
    /// every [`Finding::render`] the same way. See `crate::paint`'s module
    /// comment for why this function reads no stream itself.
    pub fn render(&self, detail: Detail, mode: crate::paint::ColorMode) -> String {
        use std::fmt::Write;
        let mut out = String::new();
        // The change first, because it is the input that decides which
        // instances reached a verdict at all, and coverage below counts the
        // ones that did not.
        if let Some(change) = &self.change {
            out.push_str(&change.render());
        }
        out.push_str(&self.coverage.render());
        // Spec 7 puts the count of open pairs "beside coverage", and spec 4
        // says what it adds there: a payload that never shrinks is visible from
        // the second run rather than at its expiry.
        out.push_str(&self.adoption.render(mode));
        // Spec 12 puts the read set here, "beside its coverage numbers". The
        // size is beside them and the union is an artifact of its own, because
        // a hash of every document is a thing a gate reads and a thing a
        // recorded report would re-bless on every edit to a paragraph.
        let _ = writeln!(out, "{}", self.read_set.summary());
        for (rule, count) in self.per_rule() {
            if count > 0 {
                let _ = writeln!(out, "  {count:5} instances of {rule}");
            }
        }

        // Spec 4 puts the suppression inventory in the coverage report, and
        // this is it. It is above the rule list rather than below the findings
        // for the reason coverage is above everything: a reader who stops here
        // has read what this run did not report as well as what it did.
        out.push_str(&self.suppressions.render());

        // What each rule sees. Spec 12 asks that the count of the barriers be a
        // number a reader can read, rather than a property discovered under
        // load, and this is that number written out per rule.
        //
        // What each rule *serves* used to be here too, one line under the
        // scope. It is in the register below now, from the obligation's side,
        // with the disposition and the control beside it. Two printings of one
        // binding is what spec 4 rules against in the declarations, and a
        // report is no different.
        let _ = writeln!(
            out,
            "{}",
            crate::paint::paint(
                crate::paint::Role::Heading,
                "rules, and for each the scope that binds it",
                mode
            )
        );
        for served in &self.served {
            let _ = writeln!(out, "  {}\n    {}", served.rule, served.scope.render());
        }

        // Spec 4 makes the register a projection of the two declarations, and
        // the coverage report — "what fraction of obligations are verified, by
        // severity, with the gap list" — generated from it. This is that.
        out.push_str(&self.register.render());
        // Beside the register for its reason: a run that only counted the
        // verifications could not show which were observed and which were
        // only declared (#937).
        out.push_str(&self.verifications.render());

        if detail != Detail::Totals {
            let _ = writeln!(out, "{} findings", self.findings.len());
            for (severity, count) in self.counts() {
                if count > 0 {
                    let _ = writeln!(
                        out,
                        "  {count:5} {}",
                        crate::paint::severity_word(severity, mode)
                    );
                }
            }
        }

        if detail == Detail::EveryInstance {
            out.push_str("\ninstances\n");
            for instance in &self.instances {
                let _ = writeln!(
                    out,
                    "  {} {}\n    {}",
                    instance.rule,
                    instance.paths().join(" + "),
                    match &instance.outcome {
                        Outcome::Passed => "passed".to_string(),
                        Outcome::Failed(_) => "failed".to_string(),
                        Outcome::Skipped(reason) => format!("skipped: {reason}"),
                    }
                );
            }
        }

        if !self.findings.is_empty() && detail != Detail::Totals {
            out.push('\n');
            for finding in &self.findings {
                out.push_str(&finding.render(mode));
            }
        }
        out
    }
}