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
//! Diagnostics: the `--json` shape from spec-0002 and the stable `DW01xx` codes.
//!
//! # One cause, one line
//!
//! **A secondary whose premise is an already-reported primary is folded into
//! that primary or suppressed, and the line that survives says how many
//! dependants it stands for.** A refusal is the whole product at the moment an
//! author meets it, and N copies of one sentence is a count the reader has to
//! discount rather than information — worse, the copies come first and bury the
//! one line that is theirs to act on.
//!
//! Measured on a 24-place campaign: deleting `layout-graph.json` printed
//! `DW0824` (correct, one line) and then **`DW0842` twenty-four times**, once
//! per `details[]` row, each saying the plan resolves 0 boxes; shortening the
//! region by five courses printed **`DW0826` twenty-four times**, once per box,
//! for one number in one document.
//!
//! The rule has two shapes, and which one applies is decided by whether the
//! secondary still has anything of its own to say:
//!
//! 1. **Fold.** Every finding shares one cause and one repair, so they are one
//!    diagnostic naming all of them. The code is unchanged and still fires per
//!    item the moment the items differ — the folded arm is reachable only in the
//!    state that makes them identical. Instances: [`codes::QUEST_NOT_EXPANDED`]
//!    when stage 5 is empty (`crate::validate`), `DW0842` at a zero box count
//!    (`compiler::detail`), `DW0826` when more than one thing leaves the region
//!    (`crate::siteplan`).
//! 2. **Defer.** The secondary is a real, separate finding whose NUMBER was
//!    measured against something already refused, so it keeps its own line and
//!    gains a clause naming what it is downstream of. Instances: `DW0818`'s
//!    clause when stage 5 declares no quests (`crate::layout`), and
//!    `crate::siteplan::off_grid_note` on every verdict computed from a box
//!    `DW0825` has refused.
//!
//! What the rule never does is drop a code's ability to refuse. Folding changes
//! how many lines say a thing, never whether the run stops: every fold above is
//! an error tier that still exits non-zero, and each has a test on both sides —
//! primary present, one line; primary absent, the secondary fires per item as
//! before.

use serde::Serialize;

/// **Which exit status a hard failure carrying this code ends the run with.**
///
/// The question this answers, and the only question it answers: *when this code
/// is what stopped the run, does the process exit 2 or 3?*
///
/// * [`ExitTier::Analysis`] — **exit 2.** The compiler did its job and the
///   CONTENT is the defect: a quest nothing can reach, a room too dark to read,
///   a wave larger than the room it spawns in. The author fixes a campaign
///   document or a prefab; nothing about the engine is wrong.
/// * [`ExitTier::Build`] — **exit 3.** The compiler could not produce a tree it
///   is willing to stand behind: geometry, navigation, the solver, the emitted
///   call graph.
///
/// # Why it lives on the code and not at the call site
///
/// The tier is a property of the RULE — `DW0210` is an analysis-tier refusal
/// wherever it is raised — and it was nonetheless re-derived from the code's
/// SPELLING at three separate places in `delvec`'s `main`, each a copy of
/// `code.id().starts_with("DW02") || code == …` with three named exceptions
/// appended. Three copies of a rule is three chances to update two of them, and
/// the spelling is not the rule: `DW0312`, `DW0313` and `DW0342` are
/// analysis-tier codes whose numbers say otherwise, which is exactly why the
/// exceptions had to be written out by hand in the first place.
///
/// # What a code that never stops a build declares
///
/// Most codes are reported as a [`Diagnostic`] among their phase's findings, and
/// the PHASE decides the exit (`validate` exits 1, `analyze` exits 2). Such a
/// code declares [`ExitTier::Build`], and that is a statement rather than a
/// placeholder: it says that IF this rule ever refuses with a build under way,
/// it stops the build. That is precisely what the string-prefix predicate did
/// for every code it did not recognise, so the declaration is the behaviour,
/// written down where the rule is.
///
/// There is no `Default` and no constructor that leaves it unsaid — a new
/// code cannot be added without answering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum ExitTier {
    /// Exit 2: the content is the defect, not the build.
    Analysis,
    /// Exit 3: the compiler could not produce a tree.
    Build,
}

impl ExitTier {
    /// The process exit status this tier ends the run with.
    ///
    /// The numbers are the CLI's stable contract (`docs/reference/compiler.md`
    /// §1): `0` ok, `1` validation, `2` analysis, `3` build.
    pub const fn exit_status(self) -> u8 {
        match self {
            ExitTier::Analysis => 2,
            ExitTier::Build => 3,
        }
    }
}

/// A stable DW diagnostic code together with the exit tier it stops a run at
/// ([`ExitTier`]) and whose state its verdict is about ([`Subject`]).
///
/// A code is not a string that a check happens to quote; it is a rule with its
/// properties, and they travel with it to every site that raises it. Every
/// rule applies to every document the engine accepts (ADR-0024): there is one
/// `dsl_version`, so a code carries nothing about when it starts binding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DwCode {
    id: &'static str,
    tier: ExitTier,
    subject: Subject,
}

impl DwCode {
    /// A rule, with the tier it exits at — see [`ExitTier`].
    pub const fn new(id: &'static str, tier: ExitTier) -> DwCode {
        DwCode {
            id,
            tier,
            subject: Subject::Campaign,
        }
    }

    /// Mark this code an **engine-property notice** — see [`Subject::Engine`]
    /// for the test to apply before choosing it. Chained onto the constructor,
    /// because the two questions are independent: *whose state is it about*,
    /// and *what does it exit with*.
    pub const fn about_the_engine(self) -> DwCode {
        DwCode {
            id: self.id,
            tier: self.tier,
            subject: Subject::Engine,
        }
    }

    /// The stable code string (`DW0180`).
    pub const fn id(self) -> &'static str {
        self.id
    }

    /// Which exit status a hard failure carrying this code ends the run with.
    pub const fn exit_tier(self) -> ExitTier {
        self.tier
    }

    /// Whose state this code's verdict is about.
    pub const fn subject(self) -> Subject {
        self.subject
    }
}

impl Serialize for DwCode {
    /// Serializes as the bare code string: a `DwCode` in a JSON payload is the
    /// `&'static str` it replaced; the tier and the subject are compiler-internal.
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(self.id)
    }
}

impl std::fmt::Display for DwCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.id)
    }
}

impl AsRef<str> for DwCode {
    fn as_ref(&self) -> &str {
        self.id
    }
}

impl PartialEq<DwCode> for String {
    fn eq(&self, other: &DwCode) -> bool {
        self == other.id
    }
}

impl PartialEq<String> for DwCode {
    fn eq(&self, other: &String) -> bool {
        self.id == other
    }
}

impl PartialEq<DwCode> for str {
    fn eq(&self, other: &DwCode) -> bool {
        self == other.id
    }
}

impl PartialEq<DwCode> for &str {
    fn eq(&self, other: &DwCode) -> bool {
        *self == other.id
    }
}

impl PartialEq<&str> for DwCode {
    fn eq(&self, other: &&str) -> bool {
        self.id == *other
    }
}

/// Diagnostic severity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    /// A hard rejection.
    Error,
    /// Advisory. Reported and rendered like an error, but does **not** fail the
    /// run — `delvec` exits non-zero only on [`Severity::Error`]. Reserved for
    /// rules whose verdict depends on something the compiler cannot fully know
    /// (e.g. `DW0330`: how much text fits depends on the player's window size and
    /// GUI scale), where a hard rejection would be a guess dressed as a fact.
    Warning,
}

/// One diagnostic, serialized as one JSON object per line by `delvec --json`.
///
/// Field order matches spec-0002: `code`, `severity`, `stage`, `path`, `message`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Diagnostic {
    /// Stable machine code, e.g. `DW0101`.
    pub code: String,
    /// Severity.
    pub severity: Severity,
    /// The stage this diagnostic concerns (`world`, `npcs`, …), or empty.
    pub stage: String,
    /// JSON-pointer-ish location within the stage document.
    pub path: String,
    /// Human-readable explanation.
    pub message: String,
    /// Whose state this verdict is about, carried over from the [`DwCode`] that
    /// raised it — the key `delvec` groups its output by.
    ///
    /// Not part of the `--json` wire shape (spec-0002 fixes that at `code`,
    /// `severity`, `stage`, `path`, `message`): it decides how the run PRESENTS
    /// a diagnostic, never something a consumer reads off one.
    #[serde(skip)]
    pub subject: Subject,
}

impl Diagnostic {
    /// Build an error diagnostic.
    pub fn error(
        code: DwCode,
        stage: impl Into<String>,
        path: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Diagnostic {
            code: code.id().to_string(),
            severity: Severity::Error,
            stage: stage.into(),
            path: path.into(),
            message: message.into(),
            subject: code.subject(),
        }
    }

    /// Build a warning (advisory) diagnostic. Reported, but does not fail the run.
    pub fn warning(
        code: DwCode,
        stage: impl Into<String>,
        path: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Diagnostic {
            code: code.id().to_string(),
            severity: Severity::Warning,
            stage: stage.into(),
            path: path.into(),
            message: message.into(),
            subject: code.subject(),
        }
    }

    /// **Which of a run's three groups this line belongs in**, lowest first.
    ///
    /// The one authority on the order `delvec` prints in. See [`Subject`] for
    /// what the split is and why.
    #[must_use]
    pub fn group(&self) -> Group {
        match (self.severity, self.subject) {
            (Severity::Error, _) => Group::Refusal,
            (Severity::Warning, Subject::Campaign) => Group::AboutTheCampaign,
            (Severity::Warning, Subject::Engine) => Group::AboutTheEngine,
        }
    }
}

/// **Whose state a code's verdict is about.**
///
/// The question this answers, and the only question it answers: *if the author
/// changed nothing about their campaign and the engine's own tables were
/// finished, would this line go away?*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Subject {
    /// The campaign. Every refusal, and every advisory whose verdict is a fact
    /// about the documents in front of the author — the default, because a
    /// diagnostic is addressed to an author unless it says otherwise.
    #[default]
    Campaign,
    /// **The ENGINE**, regardless of the campaign: an engine table that is still
    /// seeded, a standard that has not been calibrated. Nothing the author can
    /// write moves it, and it is identical on every campaign the engine
    /// compiles, so it prints after the lines that ARE theirs — see [`Group`].
    ///
    /// This is not a licence to make a campaign's problem quiet. The test is
    /// whether the line would read the same on a different campaign; where it
    /// names something the author wrote, it is a [`Subject::Campaign`] verdict
    /// however advisory its tier.
    Engine,
}

/// **The order a run's diagnostics are printed in**, and the labels they are
/// printed under.
///
/// Author-actionable first, then advisories about the campaign, then notices
/// about the engine. Measured on every site-plan run before this existed: four
/// to six paragraphs saying "this is fine" or "the engine's own table is
/// provisional", ahead of the one line the author was there to act on.
///
/// Ordering only — nothing is dropped, and every code that reported before
/// reports now.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Group {
    /// A hard rejection. Yours to act on.
    Refusal,
    /// An advisory about the campaign: a measurement, or a verdict that depends
    /// on something outside the documents.
    AboutTheCampaign,
    /// A notice about this engine, true regardless of the campaign.
    AboutTheEngine,
}

impl Group {
    /// The heading this group is printed under, with `n` lines in it.
    #[must_use]
    pub fn heading(self, n: usize) -> String {
        match self {
            Group::Refusal => format!("-- {n} refusal(s): these are yours to act on"),
            Group::AboutTheCampaign => format!("-- {n} advisory(ies) about this campaign"),
            Group::AboutTheEngine => {
                format!("-- {n} notice(s) about this engine, true of any campaign")
            }
        }
    }
}

/// The stable validation diagnostic codes (catalogued in
/// `docs/reference/compiler.md` §5).
///
/// Every entry is a [`DwCode`], so every entry states its exit tier — there is
/// no way to add one that does not.
pub mod codes {
    use super::{DwCode, ExitTier};

    /// Document does not conform to its stage schema (unknown field / wrong type).
    pub const SCHEMA: DwCode = DwCode::new("DW0100", ExitTier::Build);
    /// Envelope `stage` does not match the document's slot.
    pub const STAGE_MISMATCH: DwCode = DwCode::new("DW0101", ExitTier::Build);
    /// Unsupported `dsl_version`.
    pub const DSL_VERSION: DwCode = DwCode::new("DW0102", ExitTier::Build);
    /// Inconsistent `campaign_id` across stages.
    pub const CAMPAIGN_ID_MISMATCH: DwCode = DwCode::new("DW0103", ExitTier::Build);
    /// Malformed id syntax (kebab-case / prefix).
    pub const ID_SYNTAX: DwCode = DwCode::new("DW0110", ExitTier::Build);
    /// Duplicate id within its namespace.
    pub const ID_DUPLICATE: DwCode = DwCode::new("DW0111", ExitTier::Build);
    /// Dangling reference: an id ref does not resolve.
    pub const DANGLING_REF: DwCode = DwCode::new("DW0112", ExitTier::Build);
    /// Stage-6 dialogue node unreachable from `root`.
    pub const DIALOGUE_UNREACHABLE: DwCode = DwCode::new("DW0120", ExitTier::Build);
    /// Stage-6 dialogue `root`/`next` references an unknown node.
    pub const DIALOGUE_BAD_REF: DwCode = DwCode::new("DW0121", ExitTier::Build);
    /// Stage-6 dialogue effect references an objective that is unknown, not a
    /// `talk-to`, or a `talk-to` on a different NPC (foreign effect).
    pub const DIALOGUE_BAD_OBJECTIVE: DwCode = DwCode::new("DW0122", ExitTier::Build);
    /// A stage-5 `talk-to` objective has no reachable completing dialogue option
    /// (the static half of the compiler's `DW0203` deadlock guarantee).
    pub const DIALOGUE_UNCOVERED: DwCode = DwCode::new("DW0123", ExitTier::Build);
    /// Quest dependency cycle.
    pub const PLAN_CYCLE: DwCode = DwCode::new("DW0130", ExitTier::Build);
    /// `finale` is not a declared quest.
    pub const FINALE_UNKNOWN: DwCode = DwCode::new("DW0131", ExitTier::Build);
    /// `finale` is not the convergent sink of the plan: some declared quest is
    /// not a transitive dependency of it.
    ///
    /// **The name deliberately does not contain `FINALE_UNREACHABLE`, which
    /// belongs to `DW0201`.** That code says the finale can never complete; this
    /// one says nothing at all about the finale being reachable — in the fixture
    /// that raises it the finale completes perfectly well and a side trip hangs
    /// off the plan. Both are `DwCode`, so nothing but the name distinguishes
    /// them at a call site, and `tools/check-dw-codes.py` credits a bare
    /// constant name mentioned in a crate's tests to **that crate's** code — so
    /// one shared name would buy coverage for whichever rule the file happens to
    /// sit next to.
    pub const PLAN_NOT_CONVERGENT: DwCode = DwCode::new("DW0132", ExitTier::Build);
    /// An optional quest inside the finale's dependency closure (spec-0051
    /// §8.1) — including a finale that declares itself optional.
    pub const OPTIONAL_ON_SPINE: DwCode = DwCode::new("DW0866", ExitTier::Build);
    /// A mandatory quest whose `depends_on` edge or stage-5 `quest-complete`
    /// trigger names an optional quest (spec-0051 §8.2).
    pub const MANDATORY_ON_OPTIONAL: DwCode = DwCode::new("DW0867", ExitTier::Build);
    /// A mandatory objective gated on a flag only an optional quest produces
    /// (spec-0051 §8.3) — the mainline key behind participation.
    ///
    /// The participation-minimal replay (`DW0204`) is the compensating stronger
    /// check behind it; this one refuses at the edge so the message can name
    /// the strand.
    pub const MAINLINE_KEY_OPTIONAL: DwCode = DwCode::new("DW0868", ExitTier::Build);
    /// Objective `after` cycle.
    pub const AFTER_CYCLE: DwCode = DwCode::new("DW0140", ExitTier::Build);
    /// Anchor not provided by the area's bound prefab.
    pub const ANCHOR_UNRESOLVED: DwCode = DwCode::new("DW0142", ExitTier::Build);
    /// Item id not in the pinned 1.21.11 registry.
    pub const ITEM_UNKNOWN: DwCode = DwCode::new("DW0143", ExitTier::Build);
    /// (spec-0021) An `equipment` or `loot` enchantment id is not in the pinned
    /// 1.21.11 enchantment registry.
    pub const ENCHANTMENT_UNKNOWN: DwCode = DwCode::new("DW0433", ExitTier::Build);
    /// (spec-0021) An enchantment level is outside the 1..=255 range vanilla's
    /// `minecraft:enchantments` component can carry.
    pub const ENCHANTMENT_LEVEL: DwCode = DwCode::new("DW0434", ExitTier::Build);
    /// (spec-0021) Two `loot` entries target the same anchor, so one would
    /// silently overwrite the other's contents.
    pub const LOOT_DUPLICATE_ANCHOR: DwCode = DwCode::new("DW0435", ExitTier::Build);
    /// (spec-0021) A `loot` declaration carries more stacks than the container
    /// it fills has slots.
    pub const LOOT_TOO_MANY_ITEMS: DwCode = DwCode::new("DW0432", ExitTier::Build);
    /// A single-slot fill's `count` exceeds the item's `minecraft:max_stack_size`
    /// in the pinned 1.21.11 registry. `item replace … container.<n> with <item>
    /// <count>` fails **silently** above the cap, shipping an empty slot.
    pub const ITEM_COUNT_OVER_STACK: DwCode = DwCode::new("DW0436", ExitTier::Build);
    /// An `interact` declares `missing_item_hint` without a `requires_item`: the
    /// hint answers a gate that does not exist, so it could never narrate.
    pub const MISSING_ITEM_HINT_WITHOUT_ITEM: DwCode = DwCode::new("DW0437", ExitTier::Build);
    /// Planned quest (stage 4) has no expansion in stage 5.
    pub const QUEST_NOT_EXPANDED: DwCode = DwCode::new("DW0150", ExitTier::Build);
    /// Stage-5 quest is not planned in stage 4.
    pub const QUEST_NOT_PLANNED: DwCode = DwCode::new("DW0151", ExitTier::Build);
    /// Stage-2 NPC has no stage-6 dialogue tree.
    pub const NPC_WITHOUT_TREE: DwCode = DwCode::new("DW0152", ExitTier::Build);
    /// Stage-6 dialogue tree references an NPC not declared in stage 2.
    pub const TREE_WITHOUT_NPC: DwCode = DwCode::new("DW0153", ExitTier::Build);
    /// Area binds neither or both of `prefab` / `prefab_pool` (exactly one
    /// required).
    pub const PREFAB_BINDING: DwCode = DwCode::new("DW0160", ExitTier::Build);
    /// Area `prefab_pool` references a pool absent from `prefabs/` metadata.
    pub const POOL_UNKNOWN: DwCode = DwCode::new("DW0161", ExitTier::Build);
    /// Area `prefab` names a piece absent from `prefabs/` metadata — the same
    /// obligation [`POOL_UNKNOWN`] carries on the other arm of the binding. It
    /// is an error rather than a deferral because an area whose piece is absent
    /// contributes no anchor set at all, so every per-area anchor proof over it
    /// is SKIPPED rather than failed: a misspelling here is strictly less
    /// checked than a correct name.
    pub const PREFAB_UNKNOWN: DwCode = DwCode::new("DW0856", ExitTier::Build);
    /// (v0.6, spec-0017) A stage-7 edit script is structurally invalid: an edit
    /// names a region no earlier `select` in its batch defined, a composition
    /// (`union`/`intersect`/`subtract`) lists too few regions, a box `min`
    /// exceeds `max` on an axis, a surface band's `from` exceeds `to`, a palette
    /// recipe is empty / carries a non-positive or non-finite weight or `scale`,
    /// a `matching` list is empty, or a morph `by`/`passes` is 0. (Unknown block
    /// ids in recipes reuse [`BLOCK_UNKNOWN`] / `DW0193`; id-syntax and
    /// duplicate-name violations reuse `DW0110`/`DW0111`.)
    pub const EDIT_INVALID: DwCode = DwCode::new("DW0162", ExitTier::Build);
    /// (v0.3) A `kill` objective or `spawn-wave` effect references a `wave/<id>`
    /// not declared in the stage-5 `waves` section (dangling wave reference).
    pub const WAVE_UNKNOWN: DwCode = DwCode::new("DW0170", ExitTier::Build);
    /// (v0.3) A declared wave is referenced by a `kill` objective but is never
    /// spawned by any `spawn-wave` effect (referenced-but-never-spawned). A wave
    /// must be spawned by some effect before its kill objective is reachable.
    pub const WAVE_NEVER_SPAWNED: DwCode = DwCode::new("DW0171", ExitTier::Build);
    /// (v0.3) A `requires_flags` entry references a `flag/<id>` that no `set-flag`
    /// effect ever produces (dangling flag reference).
    pub const FLAG_UNKNOWN: DwCode = DwCode::new("DW0172", ExitTier::Build);
    /// (spec-0016 §1) A wave declares `respawns_on_rest: true` but the campaign
    /// declares no `bonfire` — nothing can ever re-seat it, so the field is a
    /// silent no-op. Either add the bonfire the re-seat is meant to hang off, or
    /// drop the field.
    pub const REST_RESEAT_NO_BONFIRE: DwCode = DwCode::new("DW0370", ExitTier::Build);
    /// (spec-0016 §1) The campaign places a `bonfire`
    /// but no class kit declares a `flask`. Resting replenishes the flask to its
    /// declared count; with no flask the rest interaction's whole recovery half
    /// is a no-op and the souls loop has no consumable to spend, so this is a
    /// build error rather than a design choice.
    pub const BONFIRE_NO_FLASK: DwCode = DwCode::new("DW0476", ExitTier::Build);
    /// **An item gate a class cannot bring.** An objective completes only for a
    /// player holding a named item, and some class's player has no way to be
    /// holding it: the item's only source in the whole campaign is *another*
    /// class's kit, or it has no source at all.
    ///
    /// A delve is played by one to four players who each pick one class, so an
    /// objective reachable only by one class's pick is an objective a party can
    /// be assembled unable to finish — and the party finds out at the thing they
    /// cannot press. Quantified over EVERY class for the same reason
    /// [`BONFIRE_NO_FLASK`] is: one class that cannot bring it is as broken as
    /// none, because a solo player of that class is a supported party.
    pub const ITEM_GATE_UNBRINGABLE: DwCode = DwCode::new("DW0849", ExitTier::Build);
    /// (spec-0016 §1) A kit item's potion `contents`
    /// is not something 1.21.11 can pour: declared on an item that carries no
    /// `minecraft:potion_contents` component, empty (neither a named potion nor
    /// an effect), an unknown potion or status-effect id, an amplifier or
    /// duration outside the field vanilla stores it in, a lasting effect with no
    /// `duration`, an instantaneous one *with* a duration, or a malformed
    /// `color`.
    pub const KIT_POTION_INVALID: DwCode = DwCode::new("DW0486", ExitTier::Build);
    /// (spec-0016 §1) A potion-bearing kit item
    /// declares no `contents` at `dsl_version` 0.8.0 — the Uncraftable Potion, a
    /// bottle that pours nothing. The placeholder flask, as a build error.
    pub const KIT_POTION_MISSING: DwCode = DwCode::new("DW0487", ExitTier::Build);
    /// A `drops[]` `slot` entry does not
    /// name a distinct slot the same entity's `equipment` actually fills — the
    /// slot is empty, or the same slot is declared twice. A mob can only leave
    /// behind a piece it wears, and it can only leave it behind once.
    pub const DROP_SLOT_UNFILLED: DwCode = DwCode::new("DW0490", ExitTier::Build);
    /// `drops[]` on an encounter that is
    /// not billed `elite` or `boss`. Only a named fight leaves anything behind;
    /// an ordinary mob's kit is never farmable (no-grind constitution), so the
    /// declaration is refused rather than silently making rank-and-file gear
    /// lootable.
    pub const DROP_NOT_TIERED: DwCode = DwCode::new("DW0491", ExitTier::Build);
    /// A `collect` `dropped_by` is not backed by the wave it names:
    /// the wave declares no `{item}` drop of this objective's item, the count
    /// asks for more copies than the wave's mobs can yield, or the objective
    /// also declares a `container` (the item cannot come out of a box *and* off
    /// a body).
    pub const DROP_COLLECT_UNSOURCED: DwCode = DwCode::new("DW0492", ExitTier::Build);
    /// A `collect` `dropped_by` is not ordered after the fight that
    /// produces it: no `kill` objective for that wave precedes this collect in
    /// the objective graph. Without that edge "kill the boss, take its key" is
    /// an authoring intention the quest graph cannot prove, and the collect
    /// reads as reachable from the campaign's first tick.
    pub const DROP_COLLECT_UNORDERED: DwCode = DwCode::new("DW0493", ExitTier::Build);
    /// (spec-0031, DSL v0.10) A `lethal_volumes[]` entry's `message` is blank.
    ///
    /// The volume would still kill — and would kill in silence, which is the one
    /// thing the declaration exists to prevent. There is no compiler default that
    /// could be right for a cliff, a lava pit and an acid pool at once, so a blank
    /// wording is refused rather than papered over: a gate that reports green
    /// while the player learns nothing is exactly the vacuous pass CLAUDE.md names.
    pub const LETHAL_MESSAGE_BLANK: DwCode = DwCode::new("DW0512", ExitTier::Build);
    /// (spec-0031, DSL v0.10) **A grant whose removal is a later effect, not its
    /// own duration.** A `give-effect` is still live at the moment a
    /// `clear-effect` for the same effect fires in the same bundle, so the clear
    /// — not the duration — is what ends it.
    ///
    /// A bundle that does not reach its end leaves the effect on the player: a
    /// logout, a crash, a death mid-chain, a `sequence` whose remaining
    /// `schedule` never runs. A duration expires with no cooperation from
    /// anything, which is why `seconds` is mandatory and why vanilla's `infinite`
    /// is absent from this surface — this diagnostic is what stops the same
    /// hazard being rebuilt out of two effects that are individually fine.
    pub const EFFECT_CLEARED_LIVE: DwCode = DwCode::new("DW0540", ExitTier::Build);
    /// (spec-0031, DSL v0.10) A `give-effect`'s `seconds` is zero or beyond
    /// [`crate::MAX_EFFECT_SECONDS`], or its `amplifier` is beyond
    /// [`crate::MAX_POTION_AMPLIFIER`].
    ///
    /// Zero is the grant that never happens — the unbound-vacuity class as a
    /// number. The ceilings are vanilla's own field widths, so a duration typed
    /// in ticks or milliseconds is caught instead of silently overflowing.
    pub const EFFECT_GRANT_BOUNDS: DwCode = DwCode::new("DW0541", ExitTier::Build);
    /// (v0.3) A wave mob `entity` is not a known vanilla entity id. (Item-id
    /// checks for `collect.item`, `interact.requires_item` and `give-item.item`
    /// reuse [`ITEM_UNKNOWN`] / `DW0143`.)
    pub const ENTITY_UNKNOWN: DwCode = DwCode::new("DW0173", ExitTier::Build);
    /// (i18n) An l10n sidecar does not correctly cover a declared language: the
    /// `l10n/<code>.json` file is absent, its envelope (`campaign_id` / `lang` /
    /// `dsl_version`) is inconsistent, or it is **missing** a key from the
    /// authoritative inventory (under-coverage). English (`en`) is implicit and
    /// never declared, so it is never checked.
    pub const L10N_MISSING: DwCode = DwCode::new("DW0180", ExitTier::Build);
    /// (i18n) An l10n sidecar carries an **orphan** key that is not in the
    /// authoritative string inventory derived from the stage docs (over-coverage).
    pub const L10N_ORPHAN: DwCode = DwCode::new("DW0181", ExitTier::Build);
    /// (i18n / harness oracle) A player-visible string — authored English or any
    /// sidecar translation — contains the reserved completion-marker sigil
    /// `[dw:complete`. That chat sequence is the validation bot's per-objective
    /// completion oracle; content carrying it could forge a passing critical-path
    /// step. The channel is reserved, not merely conventional.
    pub const MARKER_RESERVED: DwCode = DwCode::new("DW0182", ExitTier::Build);
    /// (i18n v2) A player-visible string — authored English or any sidecar
    /// translation — contains a character from the reserved private-use block the
    /// compiler uses to carry an l10n key from the stage docs to the text
    /// component it is emitted into ([`crate::l10n::TR_SIGIL`]). Content carrying
    /// it could impersonate a translation tag, or survive into the datapack and
    /// render as a tofu box. The block is reserved, not merely conventional.
    pub const TR_SIGIL_RESERVED: DwCode = DwCode::new("DW0183", ExitTier::Build);
    /// (i18n v2) A declared language has no entry in the Minecraft language-code
    /// mapping table ([`crate::l10n::mc_lang_code`]), so the resource pack has no
    /// filename to write its `assets/delvewright/lang/<code>.json` under. A
    /// language is never silently dropped: either the code is corrected to a
    /// mapped one, or the table gains the entry.
    pub const LANG_CODE_UNMAPPED: DwCode = DwCode::new("DW0184", ExitTier::Build);
    /// (i18n v2) A campaign l10n sidecar defines a key in the reserved
    /// `delvewright.` **chrome** namespace ([`crate::chrome`]). Those are the
    /// engine's own on-screen strings — `New objective: `, `Choose your class`,
    /// the default a bonfire shows — owned by the compiler, translated with it,
    /// and authored by no campaign; a sidecar row under that prefix would be
    /// written into the language file and silently replace product chrome for that
    /// language. The namespace is reserved, not merely conventional.
    pub const CHROME_RESERVED: DwCode = DwCode::new("DW0186", ExitTier::Build);
    /// (i18n v2) An l10n sidecar row was translated from English the campaign no
    /// longer holds: its `source` entry differs from the key's canonical English.
    /// The translation is present, applied and **wrong**, and no key-set check can
    /// see it — `DW0180`/`DW0181` compare key SETS, and a rewritten line moves no
    /// key. Load-bearing for entity display names, whose key belongs to the first
    /// site declaring a given text, so renaming one body can migrate a key to
    /// another body and the row that goes stale is not the one the author edited.
    pub const L10N_STALE: DwCode = DwCode::new("DW0187", ExitTier::Build);
    /// (i18n v2) An l10n sidecar records provenance for only some of its rows (or
    /// none), so `DW0187` cannot see the rest. A warning, not an error: the
    /// `source` map is additive, and this is the one-version deprecation window
    /// before it is required. It states the unguarded row count, so an
    /// unadopted sidecar is a reported number on every run rather than silence
    /// that reads like a pass.
    pub const L10N_PROVENANCE_MISSING: DwCode = DwCode::new("DW0188", ExitTier::Build);
    /// (v0.4) A mannequin NPC `skin.texture_id` is malformed (not a bare kebab
    /// token) or duplicated across NPCs (spec-0009). A missing `model` is a
    /// schema error (`DW0100`); a missing PNG is a build error (`DW0309`).
    pub const SKIN_INVALID: DwCode = DwCode::new("DW0190", ExitTier::Build);
    /// (v0.4) A `talk-to` objective has no **ungated** reachable completing
    /// dialogue option — every completing option is `requires_flags`-gated, so
    /// the objective can deadlock the moment it activates (spec-0008 §1). Keep at
    /// least one ungated completing path.
    pub const DIALOGUE_FLAG_DEADLOCK: DwCode = DwCode::new("DW0191", ExitTier::Build);
    /// (v0.4) A wave mob `effects[].effect` is not a known 1.21.11 effect id.
    pub const EFFECT_UNKNOWN: DwCode = DwCode::new("DW0192", ExitTier::Build);
    /// (v0.4) A `set-block` / `interact.prop` block id is not a known 1.21.11
    /// block id.
    pub const BLOCK_UNKNOWN: DwCode = DwCode::new("DW0193", ExitTier::Build);
    /// (v0.4) An environment trigger id is malformed (`DW0110`-style) or
    /// duplicated within the stage-5 `triggers` namespace.
    pub const TRIGGER_INVALID: DwCode = DwCode::new("DW0194", ExitTier::Build);
    /// (v0.4) A dialogue `talk-to` or `interact` objective targets an NPC after a
    /// `despawn-npc` removes it on a reachable path (spec-0008 §5).
    pub const NPC_DESPAWNED_REF: DwCode = DwCode::new("DW0195", ExitTier::Build);
    /// (v0.5) An area `lighting.min_light` is out of the 1..=14 range (spec-0010).
    pub const LIGHTING_RANGE: DwCode = DwCode::new("DW0196", ExitTier::Build);
    /// (v0.6) A stage-2 NPC declares `deferred: true` but **no** `spawn-npc` effect
    /// anywhere in the campaign ever summons it — the NPC never enters the world,
    /// so its dialogue tree and any `talk-to` on it are unreachable content. The
    /// NPC-lifecycle dual of [`NPC_DESPAWNED_REF`] / `DW0195`.
    ///
    /// (0197/0198 were *reserved* by spec-0011's draft and released when that spec
    /// renumbered to `DW0340`/`DW0341`; they were never emitted by any code.)
    pub const NPC_NEVER_SPAWNED: DwCode = DwCode::new("DW0197", ExitTier::Build);
    /// (v0.6) A `talk-to` on a `deferred` NPC activates before the NPC can exist:
    /// every `spawn-npc` for it sits in a quest that is a strict *descendant* of the
    /// objective's quest on the stage-4 DAG (and none fires from a trigger or
    /// dialogue), so the objective provably activates on an empty anchor.
    pub const NPC_SPAWNED_LATE: DwCode = DwCode::new("DW0198", ExitTier::Build);
    /// (v0.6) A `cutscene` effect's shape is invalid: it mixes the multi-shot
    /// `shots` list with the single-shot `path`/`seconds` fields, gives neither,
    /// or declares a shot with an empty camera `path`. A cutscene must resolve to
    /// at least one shot, and every shot to at least one camera position.
    pub const CUTSCENE_SHAPE: DwCode = DwCode::new("DW0199", ExitTier::Build);

    /// (v0.6) `horizon: "ocean"` declared without a `boundary` (spec-0013):
    /// validation-tier (exit 1). An infinite swimmable sea with no return rule is
    /// an authoring error. Grouped in the DW032x world/region family by domain;
    /// unlike the compiler-tier DW030x geometry codes it is raised at DSL
    /// validation, so it exits 1.
    pub const OCEAN_NO_BOUNDARY: DwCode = DwCode::new("DW0320", ExitTier::Build);
    /// (v0.6) `boundary.margin` outside the `0..=64` range (spec-0013):
    /// validation-tier (exit 1).
    pub const BOUNDARY_MARGIN: DwCode = DwCode::new("DW0321", ExitTier::Build);
    /// A stage-1 `horizon` param is out of range, or is a param of a base other
    /// than the one declared (spec-0026): validation-tier (exit 1).
    pub const HORIZON_PARAM: DwCode = DwCode::new("DW0853", ExitTier::Build);
    /// A `horizon` whose base BUILDS terrain, on a campaign that states no
    /// extent for that terrain to stand around (spec-0026): validation-tier
    /// (exit 1).
    ///
    /// A surround rings a declared extent — a site plan's `region`. A campaign
    /// that seats its pieces with `areas[]` declares none, and the union of
    /// whatever gets placed is not a substitute: it is an artifact of the
    /// compiler's fixed area stride, mostly the void between areas, so ringing
    /// it builds a mountain range around empty space.
    pub const SURROUND_NO_REGION: DwCode = DwCode::new("DW0855", ExitTier::Build);
    /// (v0.6) A `sequence` effect is nested inside another `sequence` (directly, or
    /// reachable via a nested `move-actor` `on_arrive`) — timelines do not recurse
    /// (spec-0014). Flatten the inner steps into the outer timeline.
    pub const NESTED_SEQUENCE: DwCode = DwCode::new("DW0329", ExitTier::Build);

    /// (v0.6) Trap declaration structurally invalid (spec-0011): a malformed or
    /// duplicated `trap/<id>`, an `at`/`disarm.via` that no area's prefab provides,
    /// or a trap whose `disarm.via` collides with its own trigger anchor.
    /// Validation-tier (exit 1). Renumbered off the spec's stale reserved number
    /// (0197 — since taken).
    pub const TRAP_INVALID: DwCode = DwCode::new("DW0340", ExitTier::Build);
    /// (spec-0016 §2) A `shortcut` declaration is structurally invalid: a
    /// malformed or duplicate `shortcut/<id>`, a `gate`/`unlock` anchor no area's
    /// prefab provides, or a `gate` that IS the `unlock` (the mechanism must sit
    /// on the far side, not in the doorway).
    pub const SHORTCUT_INVALID: DwCode = DwCode::new("DW0371", ExitTier::Build);
    /// (spec-0016 §2) A `close-gate` effect targets a gate a `shortcut` owns.
    /// A shortcut opens **permanently** — that is the whole pattern — so its
    /// permanence is structural: there is no verb that can put it back. Use a
    /// different gate for the point-of-no-return beat.
    pub const SHORTCUT_RESEALED: DwCode = DwCode::new("DW0372", ExitTier::Build);
    /// (spec-0016 §3) An `ambush` declaration is structurally invalid: a
    /// malformed or duplicate `ambush/<id>`, an empty `actors` list (an ambush
    /// that ambushes nobody), or the same actor listed twice (the second
    /// `spawn-actor` is a guarded no-op, so the author's intent silently halves).
    /// The telegraph is deliberately NOT required — an un-telegraphed ambush is
    /// core souls vocabulary.
    pub const AMBUSH_INVALID: DwCode = DwCode::new("DW0375", ExitTier::Build);
    /// (spec-0016 §4) A `timed-gate` declaration is structurally invalid: a
    /// malformed or duplicate `timed-gate/<id>`, an `open_ticks` or
    /// `closed_ticks` of 0 (a gate that never opens, or never closes — neither is
    /// a timing gate), a `phase` at or beyond the full cycle, or a gate another
    /// `timed-gate` or a `shortcut` already owns (two clocks fighting over one
    /// region, or a clock fighting a permanent open), or a `disarm.via` anchor no
    /// area's prefab provides / one that IS the gate anchor (the jam lever cannot
    /// live inside the span it stops).
    pub const TIMED_GATE_INVALID: DwCode = DwCode::new("DW0377", ExitTier::Build);
    /// A `close-gate` effect targets the gate of a `timed-gate` that
    /// declares a `disarm`. A disarm suppresses the clock **permanently with the
    /// gate resting open** — a jammed portcullis stays up — so, exactly like a
    /// `shortcut` (`DW0372`), its permanence is structural: there is no verb that
    /// can re-arm it. Use a different gate for the beat that must re-seal, or drop
    /// the `disarm`.
    pub const TIMED_GATE_REARMED: DwCode = DwCode::new("DW0389", ExitTier::Build);
    /// (spec-0016 §6) A wave's TD `lane` / `summon` declaration is structurally
    /// invalid or internally contradictory: an empty `waypoints` list, a
    /// waypoint anchor no area's prefab provides, a repeated consecutive
    /// waypoint, an `aggro_radius` outside `4..=64`, a mob whose
    /// `attributes.follow_range` disagrees with `aggro_radius` (they MUST be
    /// equal — a patrolling raider holds ground against a target it cannot
    /// engage), or `lane` together with `summon: aggro-edge` (a lane IS the
    /// routing; aggro-edge is its opposite).
    pub const LANE_INVALID: DwCode = DwCode::new("DW0381", ExitTier::Build);
    /// (spec-0016 §6) A lane wave contains a non-raider species. `Patrolling` /
    /// `patrol_target` are Raider NBT: on anything else they are dropped and the
    /// mob simply stands where it spawned. The admitted set is vanilla's own
    /// `#minecraft:raiders` tag, read from the vendored tag table — never a
    /// species list this engine keeps. Non-raiders use `summon: aggro-edge`
    /// instead.
    pub const LANE_NOT_RAIDER: DwCode = DwCode::new("DW0382", ExitTier::Build);
    /// (spec-0016 §6) A lane wave fields fewer than 2 mobs. A lone patroller
    /// sets `Patrolling:0b` on itself when it finds no companion within its
    /// follow range (vanilla), so a one-mob lane cancels itself.
    pub const LANE_SQUAD_TOO_SMALL: DwCode = DwCode::new("DW0383", ExitTier::Build);
    /// (spec-0016 §6) A lane `pillager` is not holding a crossbow. Its only
    /// attack goal is the crossbow goal, so a pillager that acquires a target it
    /// has no runnable attack for freezes in place indefinitely — patrol blocked
    /// by the target, nothing to run instead (live-verified deadlock).
    pub const LANE_UNARMED: DwCode = DwCode::new("DW0384", ExitTier::Build);
    /// (spec-0016 §6) A `summon: aggro-edge` wave mob declares no
    /// `attributes.follow_range`. That radius IS the summon ring — the distance
    /// at which the mob perceives the party — so it is authored, never guessed
    /// from a vanilla defaults table the compiler cannot verify.
    pub const AGGRO_EDGE_NO_RANGE: DwCode = DwCode::new("DW0385", ExitTier::Build);
    /// (v0.6) A trap dispense-payload item id is not in the pinned 1.21.11 registry
    /// (spec-0011; mirrors `DW0143`). Validation-tier (exit 1). Renumbered off the
    /// spec's stale reserved number (0198 — since taken).
    pub const TRAP_PAYLOAD_UNKNOWN: DwCode = DwCode::new("DW0341", ExitTier::Build);

    /// (spec-0022) A trap declares **no consequence at all**: neither the legacy
    /// redstone `effect` nor a command `payload`. A trap that does nothing is
    /// mute hardware the completability proofs would nonetheless reason about,
    /// so it is a content mistake, not a no-op. Validation-tier (exit 1).
    pub const TRAP_NO_CONSEQUENCE: DwCode = DwCode::new("DW0440", ExitTier::Build);
    /// (spec-0022) A `volley` `projectile` / `collapse` `falling_block` /
    /// `then_floor` id is not in the pinned 1.21.11 registry (a `projectile`
    /// must be an ENTITY id, the collapse blocks BLOCK ids).
    /// Validation-tier (exit 1).
    pub const TRAP_VERB_ID_UNKNOWN: DwCode = DwCode::new("DW0441", ExitTier::Build);
    /// (spec-0022) A `volley`'s `salvos` / `interval` is out of range (`salvos`
    /// in `1..=16`, `interval` in `1..=200`). A volley fires its whole kill zone
    /// every salvo, so the entity count is `salvos x cells`; and salvos spread
    /// wider than the interval cap stop reading as one trap event.
    /// Validation-tier (exit 1).
    pub const VOLLEY_CADENCE: DwCode = DwCode::new("DW0443", ExitTier::Build);

    /// (v0.6) A `shot_style` declaration is semantically invalid (spec-0015 shot
    /// grammar): a styled shot with no `subject`; style-only fields (`subject`,
    /// `subject_b`, `dist`, `degrees`, `bearing`) on an unstyled shot; a
    /// `subject_b` on a style other than `two-shot` (or a `two-shot` without
    /// one); `degrees` off `orbit-arc` or outside `45..=120`; `dist` outside
    /// `1..=48`; or `bearing` outside `-360..=360`. Validation-tier (exit 1).
    pub const SHOT_STYLE_INVALID: DwCode = DwCode::new("DW0348", ExitTier::Build);
    /// (v0.6) A `side-track` / `low-follow` shot whose subject has no
    /// compiler-known motion: those styles dolly *with* a moving subject, so the
    /// subject must be an NPC/actor with a matching `move-npc`/`move-actor` in
    /// the same effect group or the same `sequence` timeline (an `anchor`
    /// subject can never move). Validation-tier (exit 1). Use `locked-off` /
    /// `push-in` for a static subject instead.
    pub const SHOT_SUBJECT_UNMOVED: DwCode = DwCode::new("DW0349", ExitTier::Build);

    /// (v0.4, added round-6) A `use` trigger anchored where an NPC stands.
    /// Right-click on an NPC already belongs to its dialogue advancement; a
    /// second interaction hitbox in the same cell makes the client's entity
    /// ray-pick ambiguous, and whichever entity loses the tie is silently dead
    /// — the round-6 island soft-lock class (an exactly co-located hitbox
    /// starved the giant's dialogue of every right-click). `strike` triggers
    /// are exempt: a left-click has no dialogue meaning, so the compiler rides
    /// the trigger's tag on the NPC's own hitbox instead of summoning a second
    /// one. Validation-tier (exit 1).
    pub const USE_TRIGGER_ON_NPC: DwCode = DwCode::new("DW0350", ExitTier::Build);

    /// (v0.6, spec-0018) `world.min_players` outside the `1..=4` range. A delve is
    /// played by ONE party of 1–4 (ADR/CLAUDE.md product definition), so a declared
    /// mandatory party size can never sit outside it. Validation-tier (exit 1).
    pub const PARTY_SIZE: DwCode = DwCode::new("DW0356", ExitTier::Build);
    /// (v0.6, spec-0018) A `carrier: "one"` `give-item` sits in a bundle that is
    /// only ever reached from the **scheduler** (`move-npc`/`move-actor`
    /// `on_arrive`, a `sequence` step). `carrier: "one"` means "hand this single
    /// quest prop to the player whose action earned it"; a scheduled bundle runs
    /// with the server command source and has no acting player, so there is no
    /// defensible recipient. Give it to the whole party (drop `carrier`), or move
    /// the hand-off onto the beat that a player completes. Validation-tier (exit 1).
    pub const PARTY_CARRIER_SCHEDULED: DwCode = DwCode::new("DW0357", ExitTier::Build);

    /// (v0.6) `world.difficulty` is `peaceful`. On
    /// peaceful the server discards every hostile-category mob as it is ticked —
    /// `/summon`ed, `NoAI`, `PersistenceRequired`, all of it — so a peaceful delve
    /// is one in which every wave, every hostile actor and every ambush silently
    /// ceases to exist. There is no delve that wants that, so the keyword is
    /// refused rather than honoured. Validation-tier (exit 1).
    pub const DIFFICULTY_INVALID: DwCode = DwCode::new("DW0468", ExitTier::Build);
    /// (v0.6) A campaign fields scripted `actors[]` (an
    /// ambush desugars into these too) but **no** `waves[]` and no declared
    /// `world.difficulty`, so the compiler's historical derivation ships
    /// `difficulty=peaceful` — under which every one of those actors that is a
    /// hostile species is discarded on the tick it spawns. The compiler cannot
    /// decide the question for the author: the pinned entity registry is a
    /// membership set with no mob-category data, so "is this actor a monster" is
    /// not something it can verify rather than guess. Advisory (warning,
    /// exit 0) — declaring `world.difficulty` settles it either way.
    pub const DIFFICULTY_UNDECLARED_ACTORS: DwCode = DwCode::new("DW0469", ExitTier::Build);
    /// (spec-0016 §1, spec-0023, souls ruling 5/7: "stage bosses never respawn
    /// on rest") A wave declares BOTH `tier: boss` and `respawns_on_rest: true`.
    /// `tier` and `respawns_on_rest` are two fields on the same [`Wave`]
    /// declaration — the only place a "boss" billing and a "re-seat on rest"
    /// contract can land on one another; an [`Actor`] carries `tier` too but has
    /// no `respawns_on_rest` field at all (it is killed by hand, never re-seated
    /// by a bonfire), so this is the sole structurally expressible violation of
    /// the ruling. A rest-respawning boss re-fight breaks the retry economy the
    /// ruling exists to protect: a boss is the campaign's named fight, not
    /// trash pressure the party grinds back down every rest. Validation-tier
    /// (exit 1), `dsl::validate`. Prescription: drop `tier: boss` if the
    /// encounter really is meant to re-seat (bill it `elite` instead), or drop
    /// `respawns_on_rest` if it really is the boss.
    ///
    /// [`Wave`]: crate::stages::Wave
    /// [`Actor`]: crate::stages::Actor
    pub const BOSS_RESPAWNS_ON_REST: DwCode = DwCode::new("DW0499", ExitTier::Build);

    // -- DSL v0.10 runtime state (spec-0031) ---------------------------------

    /// (v0.10, spec-0031) A `state/<kebab>` reference — in a `requires_state`
    /// comparison or in a `set-state`/`add-state`/`clear-state` verb — names a
    /// datum the campaign never declares in the stage-5 `state` list. Unlike a
    /// flag, a datum IS declared: its scope and its initial value are facts no
    /// use site can supply, so an undeclared reference is not "a datum that
    /// happens to start at zero", it is a datum with no defined multiplayer
    /// semantics at all. Validation-tier (exit 1). Prescription: declare it, or
    /// fix the id.
    pub const STATE_UNDECLARED: DwCode = DwCode::new("DW0500", ExitTier::Build);
    /// (v0.10, spec-0031) A gate's `requires_state` reads a declared datum that
    /// **no verb anywhere in the campaign ever writes**. The datum can only ever
    /// hold its declared `initial`, so the comparison's answer was decided at
    /// authoring time and the gate is a constant wearing a condition's clothes.
    ///
    /// This is the vacuity rule at the level of one datum (CLAUDE.md: *a green
    /// gate that binds to nothing is vacuous, not a pass*) — the numeric
    /// equivalent of the bot's combat floor examining zero enemies for nineteen
    /// rounds. Validation-tier (exit 1). Prescription: write the datum somewhere
    /// (`set-state`/`add-state`/`clear-state`), or drop the comparison and say
    /// what you meant unconditionally.
    pub const STATE_NEVER_WRITTEN: DwCode = DwCode::new("DW0501", ExitTier::Build);
    /// (v0.10, spec-0031) A declared datum that **no gate anywhere in the
    /// campaign ever reads**. Either some verb writes it and nothing ever asks
    /// (the write is inert — a counter nobody consults), or nothing touches it at
    /// all (a dead declaration). Runtime state exists to be compared against; a
    /// datum with no reader is bookkeeping no player can ever observe.
    /// Validation-tier (exit 1). Prescription: gate something on it with
    /// `requires_state`, or delete the declaration and its writes.
    pub const STATE_NEVER_READ: DwCode = DwCode::new("DW0502", ExitTier::Build);
    /// (v0.10, spec-0031) A `player`-scoped datum is referenced where emission
    /// has no acting player to read or write it against.
    ///
    /// Two such places exist, and both are properties of the SITE, not of the
    /// verb: a scheduler-only bundle (a `sequence` step, a `move-npc` /
    /// `move-actor` `on_arrive`) runs with the server command source — the same
    /// seam `DW0357` polices for `carrier: "one"` — and the gates emission
    /// evaluates against the party holder rather than against a player (an
    /// objective's activation guard, a trigger's arming gate, a trap's arming
    /// gate) have no `@s` either. Validation-tier (exit 1). Prescription: declare
    /// the datum `party`-scoped if the whole party shares it, or move the
    /// read/write onto a site a player drives (a dialogue option, a cast
    /// placement, an effect on a beat a player completes).
    pub const STATE_SCOPE_UNREACHABLE: DwCode = DwCode::new("DW0503", ExitTier::Build);
    /// (v0.10, spec-0032) A `stakes[]` declaration is unusable as a personal
    /// wager: its `state` is a datum the campaign never declares, or one declared
    /// `party`-scoped.
    ///
    /// **The scope half is the multiplayer decision most likely to be made by
    /// accident** (spec-0032, stated for correction rather than left to emerge).
    /// A stake is one player's loss and one player's chance to get it back; a
    /// party-shared purse would turn a teammate's death into a penalty on
    /// everyone, and nothing in the JSON would say so. Validation-tier (exit 1).
    /// Prescription: declare the datum `player`-scoped, or point the stake at a
    /// datum that is.
    pub const STAKE_STATE_SCOPE: DwCode = DwCode::new("DW0520", ExitTier::Build);
    /// (v0.10, spec-0032) A `drop-stake` effect names a stake the campaign never
    /// declares in the stage-5 `stakes` list. Validation-tier (exit 1).
    /// Prescription: declare it, or fix the id.
    pub const STAKE_UNDECLARED: DwCode = DwCode::new("DW0521", ExitTier::Build);
    /// (v0.10, spec-0032) A declared stake that **no `drop-stake` effect anywhere
    /// in the campaign ever leaves**. The retention policy, the forfeit rule and
    /// the whole placement table are computed for a mechanism no beat can fire —
    /// a declaration wearing a feature's clothes.
    ///
    /// The same vacuity rule `DW0502` states for a datum with no reader
    /// (CLAUDE.md: *a green gate that binds to nothing is vacuous, not a pass*).
    /// Validation-tier (exit 1). Prescription: drop it from a beat — `on_death`
    /// is the usual one — or delete the declaration.
    pub const STAKE_NEVER_DROPPED: DwCode = DwCode::new("DW0522", ExitTier::Build);
    /// (v0.10, spec-0032) A `shops[].offers[]` entry that cannot deliver
    /// anything: it declares no `effects`, so its button is drawn, is pressable,
    /// and does nothing.
    ///
    /// The shop analogue of the invisible-affordance rule: a control the player
    /// can operate must have an observable answer. A refusal counts — an offer
    /// whose only effect is a gated `narrate` saying "you cannot afford that" is
    /// exactly the authored shape spec-0032 asks for. Validation-tier (exit 1).
    /// Prescription: give the offer effects, or delete it.
    pub const SHOP_OFFER_INERT: DwCode = DwCode::new("DW0523", ExitTier::Build);
    /// (v0.10, spec-0032) A `forfeit` of kind `proportion` whose `percent` is
    /// above 100 — a death that takes more than the whole purse. Validation-tier
    /// (exit 1). Prescription: 0–100, or use `all`.
    pub const STAKE_FORFEIT_RANGE: DwCode = DwCode::new("DW0524", ExitTier::Build);
    /// (v0.10, spec-0032) **A comparison read after the bundle has already changed
    /// what it compares.** An effect's `requires_state` names a datum that an
    /// EARLIER effect in the same bundle writes, so the gate is evaluated against
    /// the post-write value, not the value the beat started with.
    ///
    /// Found in the emitted output of spec-0032's own first shop. The authored
    /// shape a shop wants is "the purchase behind `at-least 1`, the apology behind
    /// `at-most 0`" — and written in that order, buying your LAST ember prints both:
    /// the debit runs, the balance falls to 0, and the apology's gate — evaluated
    /// after it — now holds. Vanilla evaluates each `execute` when it reaches it,
    /// which is the whole reason a per-effect gate is useful, so this is not a bug
    /// to fix in emission: it is an ordering hazard that only reading the generated
    /// function reveals. The fix is always the same and always local — **put the
    /// reading effect before the writing one** — which is why this is a warning
    /// naming the earlier write rather than a refusal.
    ///
    /// Warning-tier (exit 0). Prescription: move the gated effect ahead of the
    /// write, or gate it on something the bundle does not itself change.
    pub const STATE_READ_AFTER_WRITE: DwCode = DwCode::new("DW0527", ExitTier::Build);

    /// (v0.11) **A press answer addressed to a click vanilla cannot attribute.**
    /// A trigger declares `audience: presser` on something other than an
    /// `on: use`.
    ///
    /// `minecraft:player_interacted_with_entity` is the only vanilla criterion
    /// that runs a function as the player who clicked, and it fires on
    /// right-clicks alone. A left-click is recorded in the interaction entity's
    /// `attack` NBT — a UUID no command can become — and an `approach` involves no
    /// click at all. Approximating it (polling the record and assuming the nearest
    /// player) is the downstream folklore CLAUDE.md's no-hack rule excludes, so the
    /// capability is refused rather than faked.
    pub const TRIGGER_AUDIENCE_UNATTRIBUTABLE: DwCode = DwCode::new("DW0427", ExitTier::Build);

    /// (v0.11) **A trigger id in the compiler's reserved `dw-` namespace.** The
    /// compiler synthesizes triggers of its own — today the press answer every
    /// sealed gate and shortcut door gives (`trigger/dw-press-…`) — and two
    /// triggers sharing an id would share one `dw_trig_…` tag and one emitted
    /// function, so one of them would silently disappear. Reserving the prefix
    /// makes the collision impossible by construction instead of improbable.
    pub const TRIGGER_ID_RESERVED: DwCode = DwCode::new("DW0428", ExitTier::Build);

    /// (v0.11) **A sealed body with no press answer**, uniformly over the
    /// pressable class. A `shortcuts[]` door or
    /// a `close-gate`'s wall is sealed, and nothing says what it answers when the
    /// party presses it — no `use` trigger anchored on it, and (for a
    /// `close-gate`) no authored `sealed_hint`.
    ///
    /// The compiler deliberately does **not** fill that silence. A baked default
    /// is the compiler making a design statement — about tone, about what this
    /// specific door is — on the author's behalf, and then never telling them it
    /// did; an error makes the author say it. Same rule as "no hacks at any
    /// layer": if content needs a thing, the DSL exposes it and the author
    /// declares it, rather than a lower layer inventing it.
    ///
    /// One rule for the whole pressable class: two objects of the same class do
    /// not get two defaulting policies, which would be the "capability keyed to
    /// the verb" defect this very surface is CLAUDE.md's worked example of.
    pub const SEALED_BODY_UNANSWERED: DwCode = DwCode::new("DW0429", ExitTier::Build);

    /// (v0.11, spec-0034) **A declared locomotion the engine cannot hold the
    /// body to** — today exactly one value, `aquatic`.
    ///
    /// The declaration surface exists so an author can claim a capability and
    /// have the claim PROVEN. `aquatic` is the one
    /// class that carries no exemption and governs no rule: it is a ledger
    /// label the compiler derives from vanilla's own `#minecraft:aquatic` tag.
    /// Declaring it could therefore never change a verdict, so it would always
    /// land in `DW0454` — and a value whose only possible outcome is another
    /// diagnostic is a trap, not a surface.
    ///
    /// The gap it names, stated rather than left to folklore (CLAUDE.md's
    /// no-hack rule): the compiler routes **every** body on standable ground,
    /// and `flooded` cells are impassable and never floor for every body. There
    /// is no water-traversal model for a declaration to feed, so there is
    /// nothing to hold an aquatic claim to. When routing grows one, this
    /// refusal is what has to be deleted to enable the value.
    ///
    /// Error tier, raised in `validate_campaign_with`, so the run ends at the
    /// validation tier (exit 1). Prescription: remove the declaration — a body whose
    /// route crosses water is governed by the flooded-cell rules already, and
    /// the derived aquatic class still reaches the binding ledger.
    pub const TRAVERSAL_UNPROVABLE: DwCode = DwCode::new("DW0455", ExitTier::Build);

    /// A gate contradicts itself, so it can NEVER open: a flag on both its
    /// `requires_flags` and `forbids_flags`, or `requires_state` terms on one
    /// datum that no integer satisfies (`at-least 5` with `at-most 3`, two
    /// different `equals`). The thing carrying it — objective, effect, trigger,
    /// trap, dialogue option, cast placement, shop offer — is authored content
    /// that provably never happens, which is a defect in what the document
    /// SAYS, not a stylistic lint. One rule over the whole closed consumer set
    /// ([`crate::gate::for_each_gate`]), because satisfiability is a property
    /// of the gate, never of the verb that first needed the question answered.
    ///
    /// Error tier, validation (exit 1).
    pub const GATE_NEVER_OPENS: DwCode = DwCode::new("DW0847", ExitTier::Build);
}

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

    /// The tier's arithmetic is the CLI's published contract
    /// (`docs/reference/compiler.md` §1), so it is asserted rather than left to
    /// whoever next reads the `match`.
    #[test]
    fn a_tier_maps_to_its_published_exit_status() {
        assert_eq!(ExitTier::Analysis.exit_status(), 2);
        assert_eq!(ExitTier::Build.exit_status(), 3);
    }

    /// A code carries the tier it was declared with, independently of what its
    /// number happens to spell — which is the
    /// whole point of moving the tier off the code's spelling. `DW0312` is the
    /// live instance: a `DW03xx` number that exits 2.
    #[test]
    fn a_code_carries_the_tier_it_declares_not_the_one_its_number_spells() {
        // `let`, not `const`: a `const NAME: DwCode = …` here would be a SECOND
        // diagnostic constant declaring a live code, and `tools/check-dw-codes.py`
        // reads every `crates/**/*.rs` — it refuses one code declared twice, and
        // it is right to. Measured: this test written with `const` reds that gate
        // on all three codes.
        let analysis_spelt_dw03 = DwCode::new("DW0312", ExitTier::Analysis);
        let build_spelt_dw03 = DwCode::new("DW0311", ExitTier::Build);

        assert_eq!(analysis_spelt_dw03.exit_tier(), ExitTier::Analysis);
        assert_eq!(analysis_spelt_dw03.exit_tier().exit_status(), 2);
        assert_eq!(build_spelt_dw03.exit_tier().exit_status(), 3);
    }

    /// A code's properties are independent, and `about_the_engine`
    /// rebuilds the struct field by field — the one place where setting one
    /// could silently reset another. Today it cannot (there is no `Default` and
    /// no struct-update syntax, so an omitted field is a compile error), but
    /// "the compiler would catch it" is a claim about the current shape, and
    /// this is the assertion that survives the shape changing.
    #[test]
    fn marking_a_code_an_engine_notice_keeps_its_tier() {
        let engine_notice = DwCode::new("DW0813", ExitTier::Analysis).about_the_engine();
        assert_eq!(engine_notice.subject(), Subject::Engine);
        assert_eq!(engine_notice.exit_tier(), ExitTier::Analysis);
        assert_eq!(engine_notice.id(), "DW0813");
    }
}