delvewright-dsl 0.22.0

Staged JSON DSL types and schemas for Delvewright adventure-map campaigns — the format the delvec compiler reads.
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
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
//! The metrics standard (spec-0049 §2) — pipeline stage 0.
//!
//! One machine-readable table, engine-owned data, in two halves whose epistemic
//! status differs and is recorded per entry:
//!
//! * **Player metrics** — facts of pinned Minecraft Java 1.21.11. Not chosen,
//!   so not calibratable: walking a level cannot make a player 0.7 blocks wide.
//! * **Building metrics** — standards this project fixes. Every one carries
//!   [`BuildingEntry::calibrated`], `false` until the metrics gym's walk
//!   (spec-0049 §2.3) rules on it.
//!
//! # Why this module is in `delvewright-dsl`
//!
//! It is the crate every other one already reaches: `schem`, `grammar`, `admit`,
//! `compiler` and `render` all resolve it. A metrics module that the navigation
//! model imports but the render layer cannot would be one authority for some
//! consumers and a copy for the rest, which is the shape this table exists to
//! end. It is also the crate that owns [`crate::diagnostic`], and the stage-3
//! and stage-4 documents whose names resolve into this table are validated here.
//!
//! # One authority, structurally
//!
//! The player half is not a second table that agrees with the navigation model —
//! it **is** the navigation model's constants. `compiler::nav` imports
//! [`MAX_AUTO_STEP_16`], [`MAX_JUMP_RISE_16`] and [`FULL_16`] from here;
//! `compiler::crosshair`, `compiler::render_plan`, `compiler::view::viewer` and
//! `compiler::combat` import the body constants they used to declare. There is
//! one definition of each number in the workspace and the export re-serializes
//! it, so a player metric cannot drift from the model that proves routes.
//!
//! What that replaced is worth recording, because it is the defect this table
//! was written against rather than a hypothetical: the player eye height was
//! declared four times (`compiler::render_plan`, `compiler::view::viewer`,
//! `compiler::creator` in milli-blocks, `render::occupancy` in `f32`) and the
//! body width twice, each a literal `0.6` or `1.62` with its own doc comment
//! saying it was vanilla's. Nothing related them, so nothing could have gone red
//! had one moved.
//!
//! # Provenance is recorded per entry
//!
//! [`Provenance`] says where a number came from, and the four values are
//! deliberately not interchangeable: an [`Provenance::EngineConstant`] cannot
//! drift because there is nothing to drift from, a [`Provenance::VanillaRule`] is
//! a claim about the game this repository has **not** measured on a running
//! server, a [`Provenance::Derived`] carries its arithmetic in its note, and a
//! [`Provenance::Provisional`] is a seed for the gym and is not a standard yet.
//! Dressing the last as one of the first three is the failure this field exists
//! to make impossible to commit silently.
//!
//! # A provisional value cannot be consumed quietly
//!
//! [`BuildingEntry::value`] is the only way to read a building metric's number,
//! and it takes `&mut `[`Reads`]. So a verdict that rests on an uncalibrated
//! standard has, by construction, recorded that it did, and [`Metrics::notice`]
//! turns that ledger into `DW0813`. The obligation lives in the signature, not
//! in a line of documentation somebody has to remember.
//!
//! The residual, named rather than implied: a caller that constructs its own
//! [`Reads`], reads through it and drops it has bypassed the notice. That is a
//! deliberate act and not the omission the rule exists to catch — nothing
//! *forgets* to thread a ledger it had to construct.
//!
//! On the campaign path it is closed rather than merely narrow.
//! [`crate::validate::validate_campaign_with`] constructs **one** ledger and
//! threads it through the stage-3 and stage-4 checks together, so every building
//! metric either of them rests a verdict on lands in the ledger the notice
//! reads. There is no second ledger for a read to disappear into.

use std::collections::{BTreeMap, BTreeSet};

use serde::Serialize;

use crate::diagnostic::{Diagnostic, DwCode, ExitTier};

/// `DW0812`: a document names a metrics entry the table does not define — a
/// `size_class`, an `opening` or a `pitch` that resolves to nothing.
pub const DW_METRIC_UNKNOWN: DwCode = DwCode::new("DW0812", ExitTier::Build);

/// `DW0813`: a verdict rests on a standard the gym has not walked.
///
/// This rule asks the campaign for nothing at all. It reports a property of the
/// ENGINE's own table — that some number a check just used is a seed rather than
/// a standard — so no campaign could adopt its way out of it. It is a warning
/// (exit 0) for the same reason: a provisional number is still a number,
/// the check still refuses, and what the line adds is that the green rests on
/// something nobody has walked.
pub const DW_METRIC_PROVISIONAL: DwCode = DwCode::new("DW0813", ExitTier::Build).about_the_engine();

// ---------------------------------------------------------------------------
// The player half — the one definition of each constant in this workspace.
// ---------------------------------------------------------------------------

/// The table's own revision. Bumped whenever the exported JSON changes by a
/// single byte **from a version that has merged**, which
/// `crates/dsl/tests/metrics.rs` enforces against a committed digest: a consumer
/// that pins a version is pinning values, and values that move under a fixed
/// version are the drift the pin was bought to prevent.
///
/// The qualifier is a scope, not an escape hatch, and the difference is worth
/// the sentence: the harm exists only where something could already have pinned
/// the number, so a branch still authoring the version it introduces has nothing
/// to break. It is also not a claim anybody makes about their own change —
/// whether a version has merged is a fact of `origin/main`.
///
/// No document declares a metrics version and no surface is gated by one. What
/// it needs is that the number cannot stand still while the table moves, and
/// that is the digest test.
pub const METRICS_VERSION: u32 = 2;

/// Player collision-box width in blocks (`0.6 × 0.6 × 1.8` standing).
pub const PLAYER_WIDTH: f64 = 0.6;

/// Player collision-box height in blocks, standing.
pub const PLAYER_HEIGHT: f64 = 1.8;

/// Player collision-box height in blocks, crouched.
pub const PLAYER_CROUCHED_HEIGHT: f64 = 1.5;

/// Player eye height above the floor of the cell the body stands in.
pub const PLAYER_EYE_HEIGHT: f64 = 1.62;

/// Player maximum health, in half-heart damage points.
pub const PLAYER_MAX_HEALTH: f64 = 20.0;

/// The largest rise, in sixteenths of a block, a walker crosses **without
/// jumping** — vanilla's player `maxUpStep` is 0.6 blocks, and 9/16 = 0.5625 is
/// the largest sixteenth under it (10/16 = 0.625 already needs a jump). A rise
/// within this budget needs no headroom above the *source* cell: the player walks
/// straight up onto a slab or a path edge.
pub const MAX_AUTO_STEP_16: i64 = 9;

/// The largest rise a walker can reach **by jumping**, in sixteenths. A vanilla
/// player's jump apex is ≈1.2522 blocks, so a surface 20/16 = 1.25 up is
/// reachable and 21/16 = 1.3125 is not. This is the bound that makes the
/// **1.5-block** slab-to-full-block step-up the impossible move it is.
pub const MAX_JUMP_RISE_16: i64 = 20;

/// A full block's height in sixteenths.
pub const FULL_16: i64 = 16;

// One number, one definition: a full block is 16/16 here and in the collision
// table this feeds, and this refuses to compile the day the two drift. It is
// asserted on this side because `blockshape` must compile knowing nothing about
// the crate around it — the prefab generators are a separate workspace and reach
// it through `prefab-invariants`, which re-exports it.
const _: () = assert!(crate::blockshape::FULL_HEIGHT_16 as i64 == FULL_16);

/// What a walker has to do to gain a given rise — the engine's ONE answer, and
/// the reason it is here rather than in either walk.
///
/// Two walks ask this question of two different object classes. The compiler's
/// navigation model asks it of an assembled world, where a floor has a real
/// collision top and a body has a footprint; `delvec::schem`'s walk asks it of a
/// box of cells with a passability answer for each, which is what a grammar
/// expansion, a structure template read off disk and a reassembled zone all are.
/// The rule belongs to neither walk: it is a fact about the pinned game's body,
/// and this crate is where the three numbers it is written in terms of already
/// live — so the rule lives beside them, and both walks read one answer.
///
/// The two callers still measure the rise differently, and that is a difference
/// of **measurement**, never of rule: a box of cells with no collision heights
/// can only read a rise as whole cells, which over-states every partial-block
/// step and therefore only ever refuses. The rise is the input; this is the rule.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rise {
    /// Inside [`MAX_AUTO_STEP_16`]: the body walks straight up, never leaves the
    /// ground, and needs no room over its head.
    Walk,
    /// Past the auto-step budget and inside [`MAX_JUMP_RISE_16`]: the body jumps,
    /// so the cell its head sweeps through must be clear or it head-bonks.
    Jump,
    /// Past the jump apex. No player makes this step.
    Beyond,
}

/// Classify a rise between two standing surfaces, in sixteenths. Negative and
/// zero rises are [`Rise::Walk`] — stepping down and walking level ask nothing
/// of the ceiling.
#[must_use]
pub fn classify_rise_16(rise_16: i64) -> Rise {
    if rise_16 <= MAX_AUTO_STEP_16 {
        Rise::Walk
    } else if rise_16 <= MAX_JUMP_RISE_16 {
        Rise::Jump
    } else {
        Rise::Beyond
    }
}

/// Can a body make this step? `head_clear` answers *is the cell the head sweeps
/// through clear at the source* — it is consulted only for a [`Rise::Jump`], so a
/// caller may compute it lazily.
///
/// This predicate is the whole of the step rule. A walk that answers "can a body
/// get from here to there" without going through it is a second opinion, and the
/// two this engine had disagreed by exactly this term: one of them omitted the
/// head sweep, so it connected a full-block rise under a two-course ceiling that
/// the other refuses, and the gate that admits a prefab proved its **positive**
/// reachability claim over the looser of the two.
#[must_use]
pub fn step_allowed(rise_16: i64, head_clear: impl FnOnce() -> bool) -> bool {
    match classify_rise_16(rise_16) {
        Rise::Walk => true,
        Rise::Jump => head_clear(),
        Rise::Beyond => false,
    }
}

/// Ticks a jumping player spends off the ground, apex to landing included.
pub const JUMP_AIRBORNE_TICKS: f64 = 12.0;

/// Player walking speed on the flat, in blocks per second (not sprinting).
pub const WALK_SPEED_BLOCKS_PER_SECOND: f64 = 4.317;

/// Server ticks per second.
pub const TICKS_PER_SECOND: f64 = 20.0;

/// The fall distance in blocks below which vanilla deals no fall damage: damage
/// is `ceil(distance − 3)` points, so a 3-block fall is free and a 4-block fall
/// costs one.
pub const FALL_DAMAGE_ONSET_BLOCKS: f64 = 3.0;

/// Ticks a walking player spends crossing one block on the flat.
///
/// Derived rather than stored, because both operands are facts and nothing
/// downstream decides a route on this number — it is the pacing denominator.
#[must_use]
pub fn walk_ticks_per_block() -> f64 {
    TICKS_PER_SECOND / WALK_SPEED_BLOCKS_PER_SECOND
}

/// The largest fall an unarmoured player at full health survives, in blocks.
///
/// `ceil(d − 3) < 20` holds up to `d = 22`, which lands on one half-heart; 23
/// blocks deals 20 and kills. Derived from [`FALL_DAMAGE_ONSET_BLOCKS`] and
/// [`PLAYER_MAX_HEALTH`] so that moving either moves this, and it exists so the
/// designed-drop policy beside it has a physical ceiling to be **tighter than**
/// rather than a number chosen next to nothing.
#[must_use]
pub fn unarmoured_survivable_fall_blocks() -> f64 {
    (FALL_DAMAGE_ONSET_BLOCKS + PLAYER_MAX_HEALTH - 1.0).floor()
}

/// Horizontal cells a standing body needs to pass: `ceil(0.6)`.
#[must_use]
pub fn passable_width_cells() -> u32 {
    PLAYER_WIDTH.ceil() as u32
}

/// Vertical cells a standing body needs to pass: `ceil(1.8)`.
///
/// This is a **player** metric and the spec's building half listed it, which is
/// the correction worth naming: the width and clearance at which a body can pass
/// at all are functions of the collision box, so no walk can change them and
/// `calibrated` would mean nothing on them. What the gym calibrates is the
/// *designed* minimum — a way class's `min_width` and `min_clearance` — which is
/// a comfort judgement and can never be chosen below this floor —
/// [`Metrics::self_check`] is what holds it there, over every way class the
/// table defines rather than over the two entries that used to stand alone.
#[must_use]
pub fn passable_clearance_cells() -> u32 {
    PLAYER_HEIGHT.ceil() as u32
}

// ---------------------------------------------------------------------------
// Entry shapes
// ---------------------------------------------------------------------------

/// Where a number came from. The four are not interchangeable — see the module
/// docs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Provenance {
    /// The number **is** a constant this module defines and the rest of the
    /// workspace imports. One definition, so nothing to drift from.
    EngineConstant,
    /// A stated rule of pinned Minecraft Java 1.21.11 that no engine constant
    /// held before this table, and that this repository has **not** measured on
    /// a running server. The note names the rule so the claim is checkable.
    VanillaRule,
    /// Computed from other entries; the note carries the arithmetic.
    Derived,
    /// A seed for the metrics gym's calibration walk. Chosen, not established.
    Provisional,
}

/// A named opening in the standard seam set, in cells.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Opening {
    /// Clear width, in cells.
    pub width: u32,
    /// Clear height, in cells.
    pub height: u32,
}

/// A named stair-pitch standard: a rise:run pattern together with the vanilla
/// blocks that realize it, and the per-step rise the realization actually
/// presents to a walking body.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Pitch {
    /// Blocks of rise per `run` blocks of horizontal travel.
    pub rise: u32,
    /// Blocks of horizontal travel per `rise` blocks of rise.
    pub run: u32,
    /// The rise in sixteenths that one realized tread presents. A stair block
    /// offers its lower half first, so a 1:1 stair run steps 8/16 twice per
    /// block of rise rather than 16/16 once; a slab ramp steps 8/16 as well.
    /// This is the number [`Metrics::self_check`] holds under the walk-up
    /// budget, which is what makes "standard pitch" mean *walked*, not merely
    /// *legal*.
    pub step_16: i64,
    /// The vanilla realization, named so the derivation above is checkable
    /// against blocks rather than asserted.
    pub realization: &'static str,
}

/// A rung of the size-class ladder — the vocabulary a layout-graph node declares
/// and a site-plan box is judged against.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct SizeClass {
    /// Smallest interior footprint, `[x, z]` in cells, inclusive.
    pub min_footprint: [u32; 2],
    /// Largest interior footprint, `[x, z]` in cells, inclusive.
    pub max_footprint: [u32; 2],
    /// Least interior clearance, in cells.
    pub min_clearance: u32,
    /// Nominal blocks of route a body walks crossing a place of this class —
    /// the per-leg length the pacing projection sums over a critical path
    /// (spec-0049 §3.3).
    ///
    /// The code that reads it is not named here on purpose: a `DW` number in a
    /// source comment is a code as far as `tools/check-dw-codes.py` is
    /// concerned, and one whose check lands two rounds from now has no catalog
    /// row to match, so naming it early reds the docs job on a rule nothing has
    /// written yet.
    pub nominal_traverse_blocks: u32,
}

/// A **way class** — the vocabulary a layout-graph node declares for a place
/// whose footprint is bounded in one axis and free in the other: a road, a
/// causeway, a corridor, a duct (spec-0053 §3).
///
/// # Why it is a second kind of classification and not a rung
///
/// The size-class ladder classifies a place by both horizontal extents at once,
/// and that is what it is for. A route has no second extent to classify: a cut
/// ledge one body wide climbing a whole seaward face is 4 by 90, and for any
/// rung to admit it that rung would have to span 4..90 on an axis — a class in
/// which an alcove and an expanse are the same thing has stopped classifying.
/// The failure is by KIND, not by margin, which is why no calibration of the
/// ladder reaches it.
///
/// # What it bounds, and what it deliberately does not
///
/// A way class bounds the **cross-section** — the axis a body feels walking it
/// — and says nothing whatever about the run. A route's length is per-campaign
/// geometry, not a standard: a village lane, a canyon rim trail, a ship's
/// gangway and a mine gallery are the same class of thing at four wildly
/// different lengths, and a `max_length` here would be this month's map wearing
/// a standard's clothes. The run is *measured*, into pacing, and is never
/// compared against anything (spec-0053 §7).
///
/// The elongation demand a way-classed box must satisfy is therefore
/// **structural rather than a constant**: the run must exceed
/// [`Self::max_width`], which is exactly what a room cannot supply. A square box
/// can never qualify, because its "run" equals its width and one number cannot
/// both be `<= max_width` and exceed it. That is what makes "declare it a way to
/// escape the ladder" refused by the object's own shape rather than by a rule
/// the author could satisfy by choosing differently.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct WayClass {
    /// Narrowest cross-section, in cells, inclusive. Never below the physical
    /// passable width — [`Metrics::self_check`] holds it there.
    pub min_width: u32,
    /// Widest cross-section, in cells, inclusive. Doubles as the elongation
    /// floor: a way-classed box's run must **exceed** this.
    pub max_width: u32,
    /// Least interior clearance, in cells.
    pub min_clearance: u32,
}

/// The kit grid: the quantum box extents are multiples of, and the datum
/// convention that fixes what a declared `y` means.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Grid {
    /// The footprint quantum `q`, in blocks.
    pub quantum: u32,
    /// The axes extents are quantized on. Vertical extent is not quantized:
    /// storey heights are their own entries and a box's height follows them.
    pub axes: [&'static str; 2],
    /// What a box's declared datum `y` names. `floor-surface`: the walk plane is
    /// at `y`, and whatever stands in the box later puts its own floor there.
    pub datum: &'static str,
}

/// One entry's value. `untagged`, so the export reads as the number or object it
/// is rather than as a wrapper a consumer has to unpick.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
pub enum MetricValue {
    /// A count of cells, blocks or ticks.
    Count(u32),
    /// A measurement that is not a whole number of anything.
    Number(f64),
    /// A yes/no fact.
    Flag(bool),
    /// A named seam opening.
    Opening(Opening),
    /// A named stair pitch.
    Pitch(Pitch),
    /// A rung of the size-class ladder.
    SizeClass(SizeClass),
    /// A way class — a route's cross-section.
    WayClass(WayClass),
    /// The kit grid.
    Grid(Grid),
}

/// One player metric: a fact of the pinned game.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PlayerEntry {
    /// The value.
    pub value: MetricValue,
    /// What the value counts (`blocks`, `sixteenths`, `ticks`, …), or `none`.
    pub unit: &'static str,
    /// Where the number came from.
    pub provenance: Provenance,
    /// One sentence: the constant this **is**, the vanilla rule it states, or
    /// the arithmetic it was derived by.
    pub note: &'static str,
}

/// One building metric: a standard this project fixes.
///
/// The value is deliberately not a public field. [`BuildingEntry::value`] is the
/// only way to read it and it takes `&mut `[`Reads`], so nothing can rest a
/// verdict on an uncalibrated standard without saying that it did.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BuildingEntry {
    /// The entry's own key, carried so a read records itself without the caller
    /// having to repeat the name it looked up under.
    key: &'static str,
    value: MetricValue,
    /// What the value counts, or `none`.
    pub unit: &'static str,
    /// Where the number came from. Orthogonal to `calibrated`: a pitch's
    /// geometry can be derived from vanilla blocks while the judgement that it
    /// is *the* standard is still unwalked.
    pub provenance: Provenance,
    /// Whether the metrics gym's walk has ruled on this value. `false` on every
    /// entry at this version.
    pub calibrated: bool,
    /// One sentence: what the number is for and what the gym is being asked to
    /// decide about it.
    pub note: &'static str,
}

impl BuildingEntry {
    /// Read the value, recording the read.
    ///
    /// The `&mut `[`Reads`] is the whole mechanism: [`Metrics::notice`] reports
    /// exactly the uncalibrated entries a run actually consumed, so `DW0813`
    /// cannot be forgotten by a check that reads one and can never fire over a
    /// standard nothing looked at.
    #[must_use]
    pub fn value(&self, reads: &mut Reads) -> &MetricValue {
        reads.record(self);
        &self.value
    }

    /// The value with no read recorded — for rendering the table, never for
    /// deciding anything.
    ///
    /// Reachable only inside this crate, and used at exactly one site: the
    /// export, which reports the table rather than resting a verdict on it. A
    /// serialization is not a verdict, and counting it as one would put every
    /// entry in every `DW0813` line and make the code mean nothing.
    pub(crate) fn value_for_display(&self) -> &MetricValue {
        &self.value
    }

    /// The entry's key.
    #[must_use]
    pub fn key(&self) -> &'static str {
        self.key
    }
}

/// The building metrics a run's verdicts have read.
///
/// Deterministic (ADR-0006): a `BTreeSet` of `&'static str`, so the order a
/// `DW0813` line names them in is the table's own order and not the order the
/// checks happened to run in.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Reads {
    read: BTreeSet<&'static str>,
    provisional: BTreeSet<&'static str>,
}

impl Reads {
    /// A ledger nothing has read through yet.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    fn record(&mut self, entry: &BuildingEntry) {
        self.read.insert(entry.key);
        if !entry.calibrated {
            self.provisional.insert(entry.key);
        }
    }

    /// How many building metrics this run's verdicts read, and how many of those
    /// the gym has not walked.
    #[must_use]
    pub fn binding(&self) -> ReadBinding {
        ReadBinding {
            read: self.read.len(),
            provisional: self.provisional.len(),
        }
    }

    /// The uncalibrated entries read, in table order.
    #[must_use]
    pub fn provisional(&self) -> Vec<&'static str> {
        self.provisional.iter().copied().collect()
    }

    /// Every entry read, in table order.
    ///
    /// The metrics gym's coverage numerator: an entry the gym's construction
    /// never read is an entry no bay was built from, so a walk of the gym cannot
    /// rule on it. Taking it from the same ledger the reads are recorded in is
    /// what stops the gym's own coverage claim being a list somebody maintains
    /// beside the generator.
    #[must_use]
    pub fn read(&self) -> BTreeSet<&'static str> {
        self.read.clone()
    }
}

/// What a run's building-metric reads bound to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ReadBinding {
    /// Building metrics read.
    pub read: usize,
    /// Of those, entries whose `calibrated` is false.
    pub provisional: usize,
}

/// A name a document wrote that this table does not define — `DW0812`'s payload.
///
/// It carries the defined set as well as the bad name, because the author's next
/// action is choosing a real one and a refusal that only says *no* sends them to
/// read the compiler.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownMetric {
    /// The kind of entry the document was naming (`size class`, `opening`, …).
    pub kind: &'static str,
    /// The same kind, plural — carried rather than derived, because three of the
    /// six nouns end in a sibilant and `{kind}s` is wrong for them.
    pub kind_plural: &'static str,
    /// The name it wrote.
    pub named: String,
    /// Every name this table does define of that kind, in table order.
    pub defined: Vec<&'static str>,
}

impl UnknownMetric {
    /// The `DW0812` refusal, located by the caller.
    #[must_use]
    pub fn diagnostic(&self, stage: &str, path: &str) -> Diagnostic {
        let defined = if self.defined.is_empty() {
            "nothing".to_string()
        } else {
            self.defined.join(", ")
        };
        Diagnostic::error(
            DW_METRIC_UNKNOWN,
            stage,
            path,
            format!(
                "the metrics table defines no {kind} called `{named}`. The table is \
                 the single authority for this vocabulary, so a name it does not \
                 define cannot compile and no check downstream has to cope with one. \
                 Defined {plural}: {defined}. Run `delvec metrics` for the whole \
                 table, including what each entry is for.",
                kind = self.kind,
                plural = self.kind_plural,
                named = self.named,
            ),
        )
    }
}

// ---------------------------------------------------------------------------
// The table
// ---------------------------------------------------------------------------

/// The metrics standard, as exported and as read.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Metrics {
    /// The table's revision.
    pub metrics_version: u32,
    /// The Minecraft version the player half states facts about.
    pub mc_version: &'static str,
    /// Facts of the pinned game, in key order.
    pub player: BTreeMap<&'static str, PlayerEntry>,
    /// Standards this project fixes, in key order.
    pub building: BTreeMap<&'static str, BuildingEntry>,
}

/// Kinds of building entry a document can name, for [`Metrics::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricKind {
    /// A seam opening (`opening.<name>`).
    Opening,
    /// A stair pitch (`pitch.<name>`).
    Pitch,
    /// A rung of the size-class ladder (`size-class.<name>`).
    SizeClass,
    /// A way class (`way-class.<name>`) — the second kind of place
    /// classification (spec-0053 §3). A [`MetricKind`] rather than a lookup of
    /// its own for the reason [`Metrics::resolve`] gives: this is a name a
    /// DOCUMENT writes, so it goes through the one path from a name to an entry
    /// and a name the table does not define is `DW0812` here exactly as it is
    /// for a size class.
    WayClass,
    /// A storey height (`storey.<name>`).
    Storey,
    /// A pacing coefficient (`pacing.<name>`) — blocks of route per minute of
    /// play. Named like the rest of the vocabulary rather than reached by a
    /// second lookup, because [`Metrics::resolve`] is the ONE path from a name
    /// to an entry and a coefficient read any other way would be a second
    /// authority (spec-0049 §2.2). No campaign document names one; the pacing
    /// projection does.
    Pacing,
}

impl MetricKind {
    /// The key prefix entries of this kind carry.
    #[must_use]
    pub fn prefix(self) -> &'static str {
        match self {
            MetricKind::Opening => "opening.",
            MetricKind::Pitch => "pitch.",
            MetricKind::SizeClass => "size-class.",
            MetricKind::WayClass => "way-class.",
            MetricKind::Storey => "storey.",
            MetricKind::Pacing => "pacing.",
        }
    }

    /// What the kind is called in a refusal, **plural**.
    ///
    /// A fact about the kind rather than an `s` appended where a message needed
    /// one: three of the six nouns end in a sibilant, so `{noun}s` reads
    /// `size classs`, `stair pitchs` and `way classs`. Written out here, a kind
    /// added later cannot inherit that by default — it has to answer.
    #[must_use]
    pub fn plural(self) -> &'static str {
        match self {
            MetricKind::Opening => "seam openings",
            MetricKind::Pitch => "stair pitches",
            MetricKind::SizeClass => "size classes",
            MetricKind::WayClass => "way classes",
            MetricKind::Storey => "storey heights",
            MetricKind::Pacing => "pacing coefficients",
        }
    }

    /// What the kind is called in a refusal.
    #[must_use]
    pub fn noun(self) -> &'static str {
        match self {
            MetricKind::Opening => "seam opening",
            MetricKind::Pitch => "stair pitch",
            MetricKind::SizeClass => "size class",
            MetricKind::WayClass => "way class",
            MetricKind::Storey => "storey height",
            MetricKind::Pacing => "pacing coefficient",
        }
    }
}

fn player(
    value: MetricValue,
    unit: &'static str,
    provenance: Provenance,
    note: &'static str,
) -> PlayerEntry {
    PlayerEntry {
        value,
        unit,
        provenance,
        note,
    }
}

fn building(
    key: &'static str,
    value: MetricValue,
    unit: &'static str,
    provenance: Provenance,
    note: &'static str,
) -> (&'static str, BuildingEntry) {
    (
        key,
        BuildingEntry {
            key,
            value,
            unit,
            provenance,
            // Every entry lands uncalibrated. The gym walk flips it, one entry
            // at a time, and that walk is the only thing that may.
            calibrated: false,
            note,
        },
    )
}

impl Metrics {
    /// The table.
    ///
    /// Rebuilt per call rather than held in a `static`, because the values are a
    /// handful of maps and a `static` would need interior mutability the moment
    /// the gym's walk starts flipping `calibrated`. Deterministic by
    /// construction: `BTreeMap`, no clock, no environment.
    #[must_use]
    pub fn table() -> Self {
        let player_entries: Vec<(&'static str, PlayerEntry)> = vec![
            (
                "body.width",
                player(
                    MetricValue::Number(PLAYER_WIDTH),
                    "blocks",
                    Provenance::EngineConstant,
                    "The standing collision box is 0.6 wide; `dsl::metrics::PLAYER_WIDTH` \
                     is the definition `compiler::nav`'s entity dims table and \
                     `compiler::crosshair` both import.",
                ),
            ),
            (
                "body.height",
                player(
                    MetricValue::Number(PLAYER_HEIGHT),
                    "blocks",
                    Provenance::EngineConstant,
                    "The standing collision box is 1.8 tall; the same definition the \
                     entity dims table reads for `minecraft:player`.",
                ),
            ),
            (
                "body.crouched-height",
                player(
                    MetricValue::Number(PLAYER_CROUCHED_HEIGHT),
                    "blocks",
                    Provenance::VanillaRule,
                    "A sneaking player's collision box is 1.5 tall, which is what lets a \
                     body pass under a cell a standing one cannot. No engine constant \
                     held this before the table, and nothing in this workspace has \
                     measured it on a running server.",
                ),
            ),
            (
                "body.eye-height",
                player(
                    MetricValue::Number(PLAYER_EYE_HEIGHT),
                    "blocks",
                    Provenance::EngineConstant,
                    "The eye sits 1.62 above the floor of the cell the body stands in. \
                     This is the definition the render plan, the viewer page and the \
                     creator overlay all import, having each declared their own before.",
                ),
            ),
            (
                "body.max-health",
                player(
                    MetricValue::Number(PLAYER_MAX_HEALTH),
                    "half-hearts",
                    Provenance::EngineConstant,
                    "Twenty points of health, the definition `compiler::combat` imports \
                     for its winnability arithmetic.",
                ),
            ),
            (
                "step.walk-up",
                player(
                    MetricValue::Count(MAX_AUTO_STEP_16 as u32),
                    "sixteenths",
                    Provenance::EngineConstant,
                    "The largest rise a walker crosses without jumping: vanilla's player \
                     `maxUpStep` is 0.6 blocks and 9/16 is the largest sixteenth under \
                     it. `compiler::nav` imports this as its step rule's walk-up budget.",
                ),
            ),
            (
                "step.jump-rise",
                player(
                    MetricValue::Count(MAX_JUMP_RISE_16 as u32),
                    "sixteenths",
                    Provenance::EngineConstant,
                    "The largest rise a walker reaches by jumping: the apex is ≈1.2522 \
                     blocks, so 20/16 is reachable and 21/16 is not. `compiler::nav` \
                     imports this, and it is why a slab-to-full-block step-up of 1.5 is \
                     the impossible move it is.",
                ),
            ),
            (
                "step.block",
                player(
                    MetricValue::Count(FULL_16 as u32),
                    "sixteenths",
                    Provenance::EngineConstant,
                    "A full block in the sixteenths the step rule is denominated in, so \
                     that no comparison in the navigation model is a float.",
                ),
            ),
            (
                "jump.airborne",
                player(
                    MetricValue::Number(JUMP_AIRBORNE_TICKS),
                    "ticks",
                    Provenance::VanillaRule,
                    "A jump is airborne about twelve ticks, apex to landing. This was \
                     prose in the navigation model's elevation-weight derivation and \
                     nothing could read it; the table is where it becomes data.",
                ),
            ),
            (
                "walk.speed",
                player(
                    MetricValue::Number(WALK_SPEED_BLOCKS_PER_SECOND),
                    "blocks/second",
                    Provenance::VanillaRule,
                    "A walking player covers 4.317 blocks a second on the flat; \
                     sprinting is faster and is not the pacing basis, because a route \
                     nobody has learnt is walked.",
                ),
            ),
            (
                "walk.ticks-per-block",
                player(
                    MetricValue::Number(walk_ticks_per_block()),
                    "ticks",
                    Provenance::Derived,
                    "Twenty ticks a second over 4.317 blocks a second. Against the \
                     twelve airborne ticks of a jump this is what makes a block of \
                     climb cost about two and a half blocks of walking, which is where \
                     the navigation model's elevation weight of two comes from.",
                ),
            ),
            (
                "fluid.passable",
                player(
                    MetricValue::Flag(false),
                    "none",
                    Provenance::VanillaRule,
                    "Water and lava are impassable to every proof in this engine and are \
                     never floor either: a body cannot stand on a fluid surface, so the \
                     two sets are disjoint and both gate standability. Marked as a rule \
                     rather than an engine constant deliberately — the navigation model \
                     encodes this as the SHAPE of its occupancy sets and not as a shared \
                     `const`, so there is no single definition for this row to be, and \
                     claiming otherwise would be the overclaim the provenance field \
                     exists to prevent.",
                ),
            ),
            (
                "fall.damage-onset",
                player(
                    MetricValue::Number(FALL_DAMAGE_ONSET_BLOCKS),
                    "blocks",
                    Provenance::VanillaRule,
                    "Fall damage is `ceil(distance − 3)` points, so a three-block fall is \
                     free and a four-block fall costs one. Stated from the vanilla \
                     damage rule; this repository has not measured it on a running \
                     server.",
                ),
            ),
            (
                "fall.unarmoured-survivable",
                player(
                    MetricValue::Number(unarmoured_survivable_fall_blocks()),
                    "blocks",
                    Provenance::Derived,
                    "`ceil(distance − 3) < 20` holds up to 22 blocks, which lands a \
                     full-health unarmoured body on one half-heart; 23 deals twenty and \
                     kills. The survivable ceiling is a function of health and armour, \
                     and this is its unarmoured, full-health case — the physical bound \
                     the designed-drop policy is deliberately tighter than.",
                ),
            ),
            (
                "passable.width",
                player(
                    MetricValue::Count(passable_width_cells()),
                    "cells",
                    Provenance::Derived,
                    "`ceil(0.6)`: one cell is the narrowest a standing body fits \
                     through. This is a fact, not a standard — no walk can change it, \
                     and it is the floor the designed corridor minimum may never be \
                     chosen below.",
                ),
            ),
            (
                "passable.clearance",
                player(
                    MetricValue::Count(passable_clearance_cells()),
                    "cells",
                    Provenance::Derived,
                    "`ceil(1.8)`: two cells is the lowest a standing body passes under. \
                     Like the width beside it this is a fact rather than a standard, \
                     which is why neither carries a calibration flag.",
                ),
            ),
        ];

        let building_entries: Vec<(&'static str, BuildingEntry)> = vec![
            building(
                "grid",
                MetricValue::Grid(Grid {
                    quantum: 4,
                    axes: ["x", "z"],
                    datum: "floor-surface",
                }),
                "blocks",
                Provenance::Provisional,
                "The footprint quantum every site-plan box's horizontal extents are \
                 multiples of, and the datum convention: a box's floor SURFACE is at \
                 its declared y, and whatever stands in the box later puts its walk \
                 plane there. Four is a seed and nothing in the existing piece library \
                 argues for it — the cave tileset is odd on every axis and the keep \
                 tileset is even but not quartered — so what the gym is being asked is \
                 whether a quantum this fine buys anything a coarser one would not.",
            ),
            building(
                "way-class.corridor",
                MetricValue::WayClass(WayClass {
                    min_width: 2,
                    max_width: 4,
                    min_clearance: 3,
                }),
                "cells",
                Provenance::Provisional,
                "The narrow way: a passage, a duct, a gallery cut through rock. Its \
                 `min_width` and `min_clearance` ARE the two numbers this table used to \
                 publish as `corridor.min-width` and `corridor.min-clearance` — one \
                 cell is passable and reads as a crawlspace, two lets two bodies pass, \
                 and two blocks of clearance puts the ceiling on the walker's head — and \
                 they are fields here rather than entries of their own so that there is \
                 one authority for the narrow way rather than a class beside two loose \
                 numbers nothing could spell. The gym walks widths one, two and three \
                 and clearances two, three and four. It is also asked a question that \
                 could not be posed while these numbers were unreachable: the kit \
                 quantum beside them is 4 and every box extent is a multiple of it, so \
                 the narrowest way any plan can currently DRAW is four cells, and the \
                 walk decides whether the floor moves up or the quantum moves down.",
            ),
            building(
                "way-class.road",
                MetricValue::WayClass(WayClass {
                    min_width: 4,
                    max_width: 16,
                    min_clearance: 6,
                }),
                "cells",
                Provenance::Provisional,
                "The broad way: a village lane, a causeway, a quay, a ledge cut across a \
                 cliff face. Wide enough that a party walks it abreast and something can \
                 come the other way, which is the difference from the corridor beside it \
                 and is what the walk is being asked to place. The clearance seed is \
                 higher than the corridor's because a way this wide reads as roofless \
                 even when it is not, and a low ceiling over a broad floor is the one \
                 combination that reads as a mistake.",
            ),
            building(
                "opening.door",
                MetricValue::Opening(Opening {
                    width: 1,
                    height: 2,
                }),
                "cells",
                Provenance::Provisional,
                "The narrow seam: one body at a time, the size of a vanilla door. Seeded \
                 at the smallest opening a standing body passes, so the gym is deciding \
                 whether the tightest legal seam is one anybody wants to walk.",
            ),
            building(
                "opening.arch",
                MetricValue::Opening(Opening {
                    width: 2,
                    height: 3,
                }),
                "cells",
                Provenance::Provisional,
                "The ordinary seam between two interior places: two abreast, headroom \
                 over both.",
            ),
            building(
                "opening.passage",
                MetricValue::Opening(Opening {
                    width: 3,
                    height: 3,
                }),
                "cells",
                Provenance::Derived,
                "The three-by-three doorway the existing jigsaw socket conventions \
                 already standardize on — `cave:socket` and `tk:socket` are both this \
                 opening, so the prefab library has been built against it for as long \
                 as it has existed. Derived from that convention rather than chosen \
                 here, and still uncalibrated: what the gym decides is whether the \
                 convention is right, not what it is.",
            ),
            building(
                "opening.gateway",
                MetricValue::Opening(Opening {
                    width: 5,
                    height: 5,
                }),
                "cells",
                Provenance::Provisional,
                "The broad seam a thing of scenery scale passes: a cart, a barge, a \
                 processional. Seeded wide enough to read as an event from inside the \
                 place it opens onto, which is the judgement the walk is for.",
            ),
            building(
                "pitch.stair",
                MetricValue::Pitch(Pitch {
                    rise: 1,
                    run: 1,
                    step_16: 8,
                    realization: "minecraft:*_stairs",
                }),
                "none",
                Provenance::Derived,
                "One block of rise per block of run, realized in stair blocks. The \
                 geometry is vanilla's: a stair offers its lower half first, so the \
                 body walks two eight-sixteenth steps per block of rise and never \
                 jumps. What is uncalibrated is the comfort judgement — whether a climb \
                 this steep is one a player wants to make repeatedly.",
            ),
            building(
                "pitch.ramp",
                MetricValue::Pitch(Pitch {
                    rise: 1,
                    run: 2,
                    step_16: 8,
                    realization: "minecraft:*_slab + full block",
                }),
                "none",
                Provenance::Derived,
                "One block of rise per two of run, realized as a bottom slab then a full \
                 block. Same eight-sixteenth tread as the stair and half the pitch, so \
                 it is the gentle standard; the run it costs is what the gym weighs it \
                 on.",
            ),
            building(
                "storey.low",
                MetricValue::Count(5),
                "blocks",
                Provenance::Derived,
                "Floor course, three cells of interior clearance, ceiling course. Taken \
                 from the existing cave tileset, every passage and room of which is five \
                 blocks tall, so this is the storey the shipped library already has \
                 rather than a number invented here.",
            ),
            building(
                "storey.standard",
                MetricValue::Count(8),
                "blocks",
                Provenance::Provisional,
                "The storey an interior room of consequence gets: six cells of clearance \
                 between courses. A seed, and the walk is what says whether a room this \
                 tall reads as generous or merely as far away.",
            ),
            building(
                "storey.hall",
                MetricValue::Count(14),
                "blocks",
                Provenance::Provisional,
                "The storey a hall gets, where the height itself is the effect. The seed \
                 is deliberately at the point where volume starts costing walking time \
                 for nothing, because that is the trade the walk has to judge.",
            ),
            building(
                "size-class.alcove",
                MetricValue::SizeClass(SizeClass {
                    min_footprint: [4, 4],
                    max_footprint: [8, 8],
                    min_clearance: 3,
                    nominal_traverse_blocks: 6,
                }),
                "cells",
                Provenance::Provisional,
                "A place a body stands in rather than crosses: a shrine, a landing, a \
                 cell. The smallest rung of the ladder, and the whole ladder's bounds \
                 are seeds — what the walk fixes is where one class stops feeling like \
                 the next.",
            ),
            building(
                "size-class.room",
                MetricValue::SizeClass(SizeClass {
                    min_footprint: [8, 8],
                    max_footprint: [16, 16],
                    min_clearance: 4,
                    nominal_traverse_blocks: 12,
                }),
                "cells",
                Provenance::Provisional,
                "A place with a purpose and something in it: a guardroom, a chapel, a \
                 workshop.",
            ),
            building(
                "size-class.hall",
                MetricValue::SizeClass(SizeClass {
                    min_footprint: [16, 16],
                    max_footprint: [32, 32],
                    min_clearance: 8,
                    nominal_traverse_blocks: 24,
                }),
                "cells",
                Provenance::Provisional,
                "A place a fight or a crowd fits in, and the smallest rung whose height \
                 is doing work of its own.",
            ),
            building(
                "size-class.arena",
                MetricValue::SizeClass(SizeClass {
                    min_footprint: [32, 32],
                    max_footprint: [64, 64],
                    min_clearance: 12,
                    nominal_traverse_blocks: 48,
                }),
                "cells",
                Provenance::Provisional,
                "A place built around one encounter, with room to retreat and re-approach.",
            ),
            building(
                "size-class.expanse",
                MetricValue::SizeClass(SizeClass {
                    min_footprint: [64, 64],
                    max_footprint: [128, 128],
                    min_clearance: 16,
                    nominal_traverse_blocks: 96,
                }),
                "cells",
                Provenance::Provisional,
                "A shore, a valley floor, a cavern — a place whose job is that crossing \
                 it takes time. The rung most at risk of being a big empty room, which \
                 is what the walk is watching for.",
            ),
            building(
                "drop.max-designed-rise",
                MetricValue::Count(5),
                "blocks",
                Provenance::Provisional,
                "The deepest fall a designed one-way drop edge may declare. A policy \
                 cap, not a physical one: the unarmoured survivable fall beside it in \
                 the player half is 22 blocks, and this is far tighter on purpose, \
                 because a drop is a topology decision and should not also be a health \
                 decision. Five costs two of twenty at full health, which is the seed \
                 the walk argues with.",
            ),
            building(
                "pacing.route-blocks-per-minute",
                MetricValue::Count(60),
                "blocks/minute",
                Provenance::Provisional,
                "Blocks of route a party gets through per minute of play, once looking, \
                 fighting and backtracking are in it. Carried with NO THRESHOLD \
                 anywhere until the first walked blockout and the first full playtest \
                 calibrate it: a threshold on a number this uncertain would be defending \
                 nothing. Its upper bound is the pure-walk figure beside it, which no \
                 party achieves.",
            ),
            building(
                "pacing.walk-only-blocks-per-minute",
                MetricValue::Count((WALK_SPEED_BLOCKS_PER_SECOND * 60.0) as u32),
                "blocks/minute",
                Provenance::Derived,
                "Walking speed times sixty: what a body covers doing nothing but \
                 walking in a straight line. It exists so the route coefficient above \
                 has a ceiling that is a fact rather than another guess, and so the \
                 ratio between them is the thing the playtest actually measures.",
            ),
        ];

        Metrics {
            metrics_version: METRICS_VERSION,
            mc_version: crate::blocks::MC_VERSION,
            player: player_entries.into_iter().collect(),
            building: building_entries.into_iter().collect(),
        }
    }

    /// Resolve a name a **document** wrote to its building entry.
    ///
    /// This is the **only** path from an authored name to an entry, and it is
    /// what makes the table the single authority rather than a suggestion: a
    /// name it does not define cannot be resolved, so it cannot compile, so no
    /// check downstream ever meets one.
    ///
    /// The other half of that guarantee is that no key string is spelled outside
    /// this module. The entries no document names — the kit grid, the designed-
    /// drop cap — are reached through the accessors below rather than by looking
    /// the key up in [`Metrics::building`], which is public so that a *reporter*
    /// can walk the whole table (`delvec metrics` counts it; the tests iterate
    /// it). Reporting is not resolution: a caller that walks every entry cannot
    /// name a wrong one, and a caller that wants ONE entry has an accessor and
    /// therefore no reason to type a key.
    ///
    /// # Errors
    ///
    /// [`UnknownMetric`], which the caller turns into `DW0812` with its own
    /// stage and path.
    pub fn resolve(&self, kind: MetricKind, named: &str) -> Result<&BuildingEntry, UnknownMetric> {
        let key = format!("{}{}", kind.prefix(), named);
        self.building
            .get(key.as_str())
            .ok_or_else(|| UnknownMetric {
                kind: kind.noun(),
                kind_plural: kind.plural(),
                named: named.to_string(),
                defined: self.names_of(kind),
            })
    }

    /// The kit grid — the quantum a site-plan box's footprint is a multiple of,
    /// and the datum convention that fixes what a declared floor `y` means.
    ///
    /// One of the entries **no document names**: an author writes a number, not
    /// the word `grid`, so it has no place in [`Metrics::resolve`]'s naming
    /// vocabulary and would need a [`MetricKind`] whose prefix is the empty
    /// string — which would make `names_of` return the whole table. An accessor
    /// instead, so the key string still lives here and nowhere else.
    ///
    /// `None` only if the table stopped defining it, which
    /// [`Metrics::self_check`] reports as an internal error.
    #[must_use]
    pub fn grid(&self, reads: &mut Reads) -> Option<Grid> {
        match self.building.get("grid")?.value(reads) {
            MetricValue::Grid(g) => Some(*g),
            _ => None,
        }
    }

    /// The deepest fall a **designed** one-way drop may declare, in blocks — a
    /// policy cap, deliberately tighter than the survivability fact in the
    /// player half. See [`Metrics::grid`] for why this is an accessor.
    #[must_use]
    pub fn max_designed_drop_blocks(&self, reads: &mut Reads) -> Option<u32> {
        match self.building.get("drop.max-designed-rise")?.value(reads) {
            MetricValue::Count(n) => Some(*n),
            _ => None,
        }
    }

    /// The **widest** standard opening in the table, in cells — the floor a
    /// contact seam's span must exceed (spec-0053 §4).
    ///
    /// Derived from the table rather than seeded, and that is the whole of why
    /// the floor is honest: anything at or under this width **could have been a
    /// portal**, so a doorway declared a contact to dodge the standard set is
    /// refused by its own width. A seeded floor would be a number an author
    /// could argue with; this one is a consequence of the standard set, and it
    /// moves when the standard set moves.
    ///
    /// It walks every opening rather than naming one, so a broader standard
    /// landing tomorrow raises the floor with no edit here — the failure mode a
    /// hand-named `opening.gateway` would have is that the floor silently stops
    /// being the broadest the day a broader one is added.
    ///
    /// `None` only if the table defines no opening at all, which
    /// [`Metrics::self_check`] reports as an internal error.
    #[must_use]
    pub fn broadest_opening_width(&self, reads: &mut Reads) -> Option<u32> {
        let mut widest: Option<u32> = None;
        for name in self.names_of(MetricKind::Opening) {
            let Ok(entry) = self.resolve(MetricKind::Opening, name) else {
                continue;
            };
            if let MetricValue::Opening(o) = entry.value(reads) {
                widest = Some(widest.map_or(o.width, |w: u32| w.max(o.width)));
            }
        }
        widest
    }

    /// Every name defined for a kind, in table order.
    #[must_use]
    pub fn names_of(&self, kind: MetricKind) -> Vec<&'static str> {
        self.building
            .keys()
            .filter_map(|k| k.strip_prefix(kind.prefix()))
            .collect()
    }

    /// The `DW0813` notice for a run, or `None` when no verdict rested on an
    /// unwalked standard.
    ///
    /// `None` at a zero binding is the calibrated end state and not a vacuity:
    /// the line's job is to say that a green rests on a seed, and once the gym
    /// has walked every entry a run reads there is nothing left for it to say.
    /// The distinguishable failure — a run that read NOTHING — is reported by
    /// [`Reads::binding`], which every caller states whether or not this returns
    /// a line.
    #[must_use]
    pub fn notice(&self, reads: &Reads, stage: &str) -> Option<Diagnostic> {
        let provisional = reads.provisional();
        if provisional.is_empty() {
            return None;
        }
        let binding = reads.binding();
        Some(Diagnostic::warning(
            DW_METRIC_PROVISIONAL,
            stage,
            "",
            format!(
                "{n} of the {read} building metric(s) this run read are provisional — the \
                 metrics gym has not walked them: {names}. The checks still ran and still \
                 refuse; what is unproven is the number they refused against.",
                n = binding.provisional,
                read = binding.read,
                names = provisional.join(", "),
            ),
        ))
    }

    /// Check the table against itself and against the player half.
    ///
    /// These are verdicts, and they read building metrics through a [`Reads`], so
    /// they are also what gives `DW0813` a live binding at this version: `delvec
    /// metrics` runs them, and the notice names the seeds the consistency verdict
    /// rested on. They are the reason the mechanism is demonstrable now rather
    /// than at the round that adds the documents.
    ///
    /// A violation is an **internal error**, not a diagnostic: the table is
    /// engine data, so an inconsistent one is a defect in this file and not in
    /// anybody's campaign, and there is no author to address a refusal to.
    #[must_use]
    pub fn self_check(&self) -> SelfCheck {
        let mut reads = Reads::new();
        let mut failures: Vec<String> = Vec::new();
        let mut checked = 0usize;

        let floor_w = u64::from(passable_width_cells());
        let floor_h = u64::from(passable_clearance_cells());

        // A place that is a route must be spellable at all. Zero way classes is
        // the state spec-0053 was written to end — the metrics gym reported
        // `corridor.min-width` and `corridor.min-clearance` unreachable because
        // no document could name a place that is not a box with a size class —
        // so an empty way vocabulary is an internal error rather than a table
        // that happens to be short one kind.
        checked += 1;
        if self.names_of(MetricKind::WayClass).is_empty() {
            failures.push(
                "the table defines no way class, so no document can state a place that is \
                 a route"
                    .to_string(),
            );
        }

        // The contact floor is derived from the standard opening set (spec-0053
        // §4), so an empty set would make that floor `None` and the refusal it
        // is the floor for unable to separate a doorway from a front.
        checked += 1;
        if self.broadest_opening_width(&mut reads).is_none() {
            failures.push(
                "the table defines no standard opening, so a contact seam's width floor — \
                 the width a front must exceed to be a front rather than a door — cannot \
                 be derived"
                    .to_string(),
            );
        }

        let quantum = match self.grid(&mut reads) {
            Some(g) => {
                checked += 1;
                if g.quantum == 0 {
                    failures.push("the kit grid's quantum is zero".to_string());
                }
                g.quantum
            }
            None => {
                failures.push("the table defines no kit `grid`".to_string());
                1
            }
        };

        for (key, entry) in &self.building {
            match entry.value(&mut reads) {
                MetricValue::Opening(o) => {
                    checked += 1;
                    if u64::from(o.width) < floor_w || u64::from(o.height) < floor_h {
                        failures.push(format!(
                            "`{key}` is {}×{}, which no standing body passes ({floor_w}×{floor_h} \
                             is the floor)",
                            o.width, o.height
                        ));
                    }
                }
                MetricValue::Pitch(p) => {
                    checked += 1;
                    if p.step_16 > MAX_AUTO_STEP_16 {
                        failures.push(format!(
                            "`{key}` presents a tread of {}/16, over the {MAX_AUTO_STEP_16}/16 \
                             walk-up budget, so it is climbed by jumping and is not a standard \
                             pitch",
                            p.step_16
                        ));
                    }
                    if p.rise == 0 || p.run == 0 {
                        failures.push(format!("`{key}` has a zero rise or run"));
                    }
                }
                MetricValue::SizeClass(c) => {
                    checked += 1;
                    for (axis, lo, hi) in [
                        ("x", c.min_footprint[0], c.max_footprint[0]),
                        ("z", c.min_footprint[1], c.max_footprint[1]),
                    ] {
                        if lo > hi {
                            failures
                                .push(format!("`{key}` has a {axis} minimum above its maximum"));
                        }
                        if lo % quantum != 0 || hi % quantum != 0 {
                            failures.push(format!(
                                "`{key}` bounds its {axis} footprint at {lo}..{hi}, which is not \
                                 on the kit grid's quantum of {quantum}"
                            ));
                        }
                    }
                    if u64::from(c.min_clearance) < floor_h {
                        failures.push(format!(
                            "`{key}` allows a clearance of {}, under the passable floor of \
                             {floor_h}",
                            c.min_clearance
                        ));
                    }
                    if c.nominal_traverse_blocks == 0 {
                        failures.push(format!("`{key}` has a nominal traverse of zero"));
                    }
                }
                MetricValue::WayClass(w) => {
                    checked += 1;
                    // The two floors the freestanding `corridor.min-*` entries
                    // used to be checked at, re-asserted here against every way
                    // class rather than against the one that inherited them: a
                    // designed minimum is a comfort judgement and a standard
                    // under the physical passable size would be a standard
                    // nothing can use, which is true of a road exactly as it is
                    // of a corridor.
                    if u64::from(w.min_width) < floor_w {
                        failures.push(format!(
                            "`{key}` allows a width of {}, under the `passable.width` floor of \
                             {floor_w}",
                            w.min_width
                        ));
                    }
                    if u64::from(w.min_clearance) < floor_h {
                        failures.push(format!(
                            "`{key}` allows a clearance of {}, under the `passable.clearance` \
                             floor of {floor_h}",
                            w.min_clearance
                        ));
                    }
                    if w.min_width > w.max_width {
                        failures.push(format!(
                            "`{key}` bounds its width at {}..{}, a minimum above its maximum",
                            w.min_width, w.max_width
                        ));
                    }
                    // `max_width` is the elongation floor as well as the widest
                    // cross-section, and a box's horizontal extents are
                    // multiples of the kit quantum (`DW0825`). A `max_width` off
                    // the quantum is therefore a bound no plan can draw a way
                    // AT, which makes the widest member of the class
                    // uninstantiable and the gym unable to rule on it. The
                    // narrow bound is deliberately NOT held to the quantum: the
                    // corridor's inherited floor of 2 sits under a quantum of 4
                    // and which of those two provisional numbers moves is the
                    // walk's to decide, not this file's.
                    if !w.max_width.is_multiple_of(quantum) {
                        failures.push(format!(
                            "`{key}` bounds its width at {}, which is not on the kit grid's \
                             quantum of {quantum}, so no box can be drawn at the widest member \
                             of the class",
                            w.max_width
                        ));
                    }
                }
                _ => {}
            }
        }

        for (key, floor) in [
            ("storey.low", floor_h + 2),
            ("storey.standard", floor_h + 2),
            ("storey.hall", floor_h + 2),
        ] {
            let Some(entry) = self.building.get(key) else {
                failures.push(format!("the table defines no `{key}`"));
                continue;
            };
            checked += 1;
            if let MetricValue::Count(n) = entry.value(&mut reads)
                && u64::from(*n) < floor
            {
                failures.push(format!(
                    "`{key}` is {n} blocks, which leaves no passable interior between a \
                     floor course and a ceiling course ({floor} is the floor)"
                ));
            }
        }

        // The policy cap is deliberately tighter than the physical one. A cap
        // that reached the survivability ceiling would not be a policy.
        if let Some(n) = self.max_designed_drop_blocks(&mut reads) {
            checked += 1;
            let physical = unarmoured_survivable_fall_blocks();
            if f64::from(n) >= physical {
                failures.push(format!(
                    "`drop.max-designed-rise` is {n} blocks, at or past the unarmoured \
                     survivable fall of {physical}, so it is not a policy cap at all"
                ));
            }
        }

        // A party cannot out-pace a body walking in a straight line.
        if let (Some(route), Some(walk)) = (
            self.building.get("pacing.route-blocks-per-minute"),
            self.building.get("pacing.walk-only-blocks-per-minute"),
        ) {
            checked += 1;
            if let (MetricValue::Count(r), MetricValue::Count(w)) =
                (route.value(&mut reads), walk.value(&mut reads))
                && r > w
            {
                failures.push(format!(
                    "`pacing.route-blocks-per-minute` is {r}, over the pure-walk ceiling of {w}"
                ));
            }
        }

        SelfCheck {
            binding: SelfCheckBinding {
                invariants: checked,
                entries: self.building.len(),
                reads: reads.binding(),
            },
            reads,
            failures,
        }
    }
}

/// What [`Metrics::self_check`] examined and what it found.
#[derive(Debug, Clone, PartialEq)]
pub struct SelfCheck {
    /// The ledger the verdicts read through, for [`Metrics::notice`].
    pub reads: Reads,
    /// What the run bound to. Stated whether or not anything failed, because a
    /// check that examined nothing is a finding and not a pass.
    pub binding: SelfCheckBinding,
    /// Inconsistencies, each a whole sentence. Non-empty is an internal error.
    pub failures: Vec<String>,
}

/// [`Metrics::self_check`]'s binding count.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct SelfCheckBinding {
    /// Invariants evaluated.
    pub invariants: usize,
    /// Building entries in the table.
    pub entries: usize,
    /// What those invariants read.
    pub reads: ReadBinding,
}

/// The table rendered for export: every entry with its value, unit, provenance
/// and note, plus the halves' own counts.
///
/// Separate from [`Metrics`]'s own `Serialize` because the building half's value
/// is private — an export is the one consumer that reads a number without
/// resting a verdict on it.
#[must_use]
pub fn export(metrics: &Metrics) -> serde_json::Value {
    let mut player = serde_json::Map::new();
    for (k, e) in &metrics.player {
        player.insert((*k).to_string(), serde_json::json!(e));
    }
    let mut building = serde_json::Map::new();
    let mut uncalibrated = 0usize;
    for (k, e) in &metrics.building {
        if !e.calibrated {
            uncalibrated += 1;
        }
        building.insert(
            (*k).to_string(),
            serde_json::json!({
                "value": e.value_for_display(),
                "unit": e.unit,
                "provenance": e.provenance,
                "calibrated": e.calibrated,
                "note": e.note,
            }),
        );
    }
    serde_json::json!({
        "metrics_version": metrics.metrics_version,
        "mc_version": metrics.mc_version,
        "counts": {
            "player": metrics.player.len(),
            "building": metrics.building.len(),
            "uncalibrated": uncalibrated,
        },
        "player": player,
        "building": building,
    })
}

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

    #[test]
    fn the_table_is_consistent_with_itself_and_with_the_player_half() {
        let m = Metrics::table();
        let check = m.self_check();
        assert!(
            check.failures.is_empty(),
            "the shipped metrics table is inconsistent: {:?}",
            check.failures
        );
        assert!(
            check.binding.invariants > 0,
            "a self-check that evaluated nothing is vacuous, not a pass"
        );
    }

    #[test]
    fn every_building_entry_lands_uncalibrated() {
        let m = Metrics::table();
        assert!(!m.building.is_empty(), "the building half is empty");
        for (k, e) in &m.building {
            assert!(
                !e.calibrated,
                "`{k}` claims to be calibrated, but the metrics gym has not been walked"
            );
        }
    }

    #[test]
    fn a_verdict_that_reads_a_seed_raises_dw0813_naming_it() {
        let m = Metrics::table();
        let check = m.self_check();
        assert!(
            check.binding.reads.provisional > 0,
            "the self-check read no provisional entry, so DW0813 binds to nothing"
        );
        let d = m
            .notice(&check.reads, "metrics")
            .expect("a run that read a seed owes the notice");
        assert_eq!(d.code, "DW0813");
        assert_eq!(d.severity, crate::diagnostic::Severity::Warning);
        for name in check.reads.provisional() {
            assert!(
                d.message.contains(name),
                "the notice must name `{name}`, the seed a verdict rested on"
            );
        }
    }

    #[test]
    fn a_run_that_read_nothing_provisional_gets_no_notice() {
        let m = Metrics::table();
        let reads = Reads::new();
        assert!(m.notice(&reads, "metrics").is_none());
        assert_eq!(reads.binding().read, 0);
    }

    #[test]
    fn an_undefined_name_is_dw0812_and_names_what_is_defined() {
        let m = Metrics::table();
        let err = m
            .resolve(MetricKind::SizeClass, "cathedral")
            .expect_err("`cathedral` is not a size class");
        let d = err.diagnostic("layout-graph", "/nodes/0/size_class");
        assert_eq!(d.code, "DW0812");
        assert!(d.message.contains("cathedral"));
        assert!(d.message.contains("room"), "the defined set is named");
        assert_eq!(d.stage, "layout-graph");
        assert_eq!(d.path, "/nodes/0/size_class");
    }

    #[test]
    fn every_kind_resolves_at_least_one_defined_name() {
        let m = Metrics::table();
        for kind in [
            MetricKind::Opening,
            MetricKind::Pitch,
            MetricKind::SizeClass,
            MetricKind::Storey,
        ] {
            let names = m.names_of(kind);
            assert!(
                !names.is_empty(),
                "{} resolves nothing, so DW0812 would refuse every name",
                kind.noun()
            );
            for n in names {
                assert!(m.resolve(kind, n).is_ok(), "`{n}` does not resolve");
            }
        }
    }

    #[test]
    fn the_derived_player_values_are_the_arithmetic_their_notes_claim() {
        assert!((walk_ticks_per_block() - 20.0 / 4.317).abs() < f64::EPSILON);
        assert!((walk_ticks_per_block() - 4.633).abs() < 0.001);
        assert_eq!(unarmoured_survivable_fall_blocks(), 22.0);
        assert_eq!(passable_width_cells(), 1);
        assert_eq!(passable_clearance_cells(), 2);
    }
}