Skip to main content

delvewright_dsl/
diagnostic.rs

1//! Diagnostics: the `--json` shape from spec-0002 and the stable `DW01xx` codes.
2//!
3//! # One cause, one line
4//!
5//! **A secondary whose premise is an already-reported primary is folded into
6//! that primary or suppressed, and the line that survives says how many
7//! dependants it stands for.** A refusal is the whole product at the moment an
8//! author meets it, and N copies of one sentence is a count the reader has to
9//! discount rather than information — worse, the copies come first and bury the
10//! one line that is theirs to act on.
11//!
12//! Measured on a 24-place campaign: deleting `layout-graph.json` printed
13//! `DW0824` (correct, one line) and then **`DW0842` twenty-four times**, once
14//! per `details[]` row, each saying the plan resolves 0 boxes; shortening the
15//! region by five courses printed **`DW0826` twenty-four times**, once per box,
16//! for one number in one document.
17//!
18//! The rule has two shapes, and which one applies is decided by whether the
19//! secondary still has anything of its own to say:
20//!
21//! 1. **Fold.** Every finding shares one cause and one repair, so they are one
22//!    diagnostic naming all of them. The code is unchanged and still fires per
23//!    item the moment the items differ — the folded arm is reachable only in the
24//!    state that makes them identical. Instances: [`codes::QUEST_NOT_EXPANDED`]
25//!    when stage 5 is empty (`crate::validate`), `DW0842` at a zero box count
26//!    (`compiler::detail`), `DW0826` when more than one thing leaves the region
27//!    (`crate::siteplan`).
28//! 2. **Defer.** The secondary is a real, separate finding whose NUMBER was
29//!    measured against something already refused, so it keeps its own line and
30//!    gains a clause naming what it is downstream of. Instances: `DW0818`'s
31//!    clause when stage 5 declares no quests (`crate::layout`), and
32//!    `crate::siteplan::off_grid_note` on every verdict computed from a box
33//!    `DW0825` has refused.
34//!
35//! What the rule never does is drop a code's ability to refuse. Folding changes
36//! how many lines say a thing, never whether the run stops: every fold above is
37//! an error tier that still exits non-zero, and each has a test on both sides —
38//! primary present, one line; primary absent, the secondary fires per item as
39//! before.
40
41use serde::Serialize;
42
43/// **Which exit status a hard failure carrying this code ends the run with.**
44///
45/// The question this answers, and the only question it answers: *when this code
46/// is what stopped the run, does the process exit 2 or 3?*
47///
48/// * [`ExitTier::Analysis`] — **exit 2.** The compiler did its job and the
49///   CONTENT is the defect: a quest nothing can reach, a room too dark to read,
50///   a wave larger than the room it spawns in. The author fixes a campaign
51///   document or a prefab; nothing about the engine is wrong.
52/// * [`ExitTier::Build`] — **exit 3.** The compiler could not produce a tree it
53///   is willing to stand behind: geometry, navigation, the solver, the emitted
54///   call graph.
55///
56/// # Why it lives on the code and not at the call site
57///
58/// The tier is a property of the RULE — `DW0210` is an analysis-tier refusal
59/// wherever it is raised — and it was nonetheless re-derived from the code's
60/// SPELLING at three separate places in `delvec`'s `main`, each a copy of
61/// `code.id().starts_with("DW02") || code == …` with three named exceptions
62/// appended. Three copies of a rule is three chances to update two of them, and
63/// the spelling is not the rule: `DW0312`, `DW0313` and `DW0342` are
64/// analysis-tier codes whose numbers say otherwise, which is exactly why the
65/// exceptions had to be written out by hand in the first place.
66///
67/// # What a code that never stops a build declares
68///
69/// Most codes are reported as a [`Diagnostic`] among their phase's findings, and
70/// the PHASE decides the exit (`validate` exits 1, `analyze` exits 2). Such a
71/// code declares [`ExitTier::Build`], and that is a statement rather than a
72/// placeholder: it says that IF this rule ever refuses with a build under way,
73/// it stops the build. That is precisely what the string-prefix predicate did
74/// for every code it did not recognise, so the declaration is the behaviour,
75/// written down where the rule is.
76///
77/// There is no `Default` and no constructor that leaves it unsaid — a new
78/// code cannot be added without answering.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
80pub enum ExitTier {
81    /// Exit 2: the content is the defect, not the build.
82    Analysis,
83    /// Exit 3: the compiler could not produce a tree.
84    Build,
85}
86
87impl ExitTier {
88    /// The process exit status this tier ends the run with.
89    ///
90    /// The numbers are the CLI's stable contract (`docs/reference/compiler.md`
91    /// §1): `0` ok, `1` validation, `2` analysis, `3` build.
92    pub const fn exit_status(self) -> u8 {
93        match self {
94            ExitTier::Analysis => 2,
95            ExitTier::Build => 3,
96        }
97    }
98}
99
100/// A stable DW diagnostic code together with the exit tier it stops a run at
101/// ([`ExitTier`]) and whose state its verdict is about ([`Subject`]).
102///
103/// A code is not a string that a check happens to quote; it is a rule with its
104/// properties, and they travel with it to every site that raises it. Every
105/// rule applies to every document the engine accepts (ADR-0024): there is one
106/// `dsl_version`, so a code carries nothing about when it starts binding.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub struct DwCode {
109    id: &'static str,
110    tier: ExitTier,
111    subject: Subject,
112}
113
114impl DwCode {
115    /// A rule, with the tier it exits at — see [`ExitTier`].
116    pub const fn new(id: &'static str, tier: ExitTier) -> DwCode {
117        DwCode {
118            id,
119            tier,
120            subject: Subject::Campaign,
121        }
122    }
123
124    /// Mark this code an **engine-property notice** — see [`Subject::Engine`]
125    /// for the test to apply before choosing it. Chained onto the constructor,
126    /// because the two questions are independent: *whose state is it about*,
127    /// and *what does it exit with*.
128    pub const fn about_the_engine(self) -> DwCode {
129        DwCode {
130            id: self.id,
131            tier: self.tier,
132            subject: Subject::Engine,
133        }
134    }
135
136    /// The stable code string (`DW0180`).
137    pub const fn id(self) -> &'static str {
138        self.id
139    }
140
141    /// Which exit status a hard failure carrying this code ends the run with.
142    pub const fn exit_tier(self) -> ExitTier {
143        self.tier
144    }
145
146    /// Whose state this code's verdict is about.
147    pub const fn subject(self) -> Subject {
148        self.subject
149    }
150}
151
152impl Serialize for DwCode {
153    /// Serializes as the bare code string: a `DwCode` in a JSON payload is the
154    /// `&'static str` it replaced; the tier and the subject are compiler-internal.
155    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
156        s.serialize_str(self.id)
157    }
158}
159
160impl std::fmt::Display for DwCode {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.write_str(self.id)
163    }
164}
165
166impl AsRef<str> for DwCode {
167    fn as_ref(&self) -> &str {
168        self.id
169    }
170}
171
172impl PartialEq<DwCode> for String {
173    fn eq(&self, other: &DwCode) -> bool {
174        self == other.id
175    }
176}
177
178impl PartialEq<String> for DwCode {
179    fn eq(&self, other: &String) -> bool {
180        self.id == other
181    }
182}
183
184impl PartialEq<DwCode> for str {
185    fn eq(&self, other: &DwCode) -> bool {
186        self == other.id
187    }
188}
189
190impl PartialEq<DwCode> for &str {
191    fn eq(&self, other: &DwCode) -> bool {
192        *self == other.id
193    }
194}
195
196impl PartialEq<&str> for DwCode {
197    fn eq(&self, other: &&str) -> bool {
198        self.id == *other
199    }
200}
201
202/// Diagnostic severity.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
204#[serde(rename_all = "lowercase")]
205pub enum Severity {
206    /// A hard rejection.
207    Error,
208    /// Advisory. Reported and rendered like an error, but does **not** fail the
209    /// run — `delvec` exits non-zero only on [`Severity::Error`]. Reserved for
210    /// rules whose verdict depends on something the compiler cannot fully know
211    /// (e.g. `DW0330`: how much text fits depends on the player's window size and
212    /// GUI scale), where a hard rejection would be a guess dressed as a fact.
213    Warning,
214}
215
216/// One diagnostic, serialized as one JSON object per line by `delvec --json`.
217///
218/// Field order matches spec-0002: `code`, `severity`, `stage`, `path`, `message`.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
220pub struct Diagnostic {
221    /// Stable machine code, e.g. `DW0101`.
222    pub code: String,
223    /// Severity.
224    pub severity: Severity,
225    /// The stage this diagnostic concerns (`world`, `npcs`, …), or empty.
226    pub stage: String,
227    /// JSON-pointer-ish location within the stage document.
228    pub path: String,
229    /// Human-readable explanation.
230    pub message: String,
231    /// Whose state this verdict is about, carried over from the [`DwCode`] that
232    /// raised it — the key `delvec` groups its output by.
233    ///
234    /// Not part of the `--json` wire shape (spec-0002 fixes that at `code`,
235    /// `severity`, `stage`, `path`, `message`): it decides how the run PRESENTS
236    /// a diagnostic, never something a consumer reads off one.
237    #[serde(skip)]
238    pub subject: Subject,
239}
240
241impl Diagnostic {
242    /// Build an error diagnostic.
243    pub fn error(
244        code: DwCode,
245        stage: impl Into<String>,
246        path: impl Into<String>,
247        message: impl Into<String>,
248    ) -> Self {
249        Diagnostic {
250            code: code.id().to_string(),
251            severity: Severity::Error,
252            stage: stage.into(),
253            path: path.into(),
254            message: message.into(),
255            subject: code.subject(),
256        }
257    }
258
259    /// Build a warning (advisory) diagnostic. Reported, but does not fail the run.
260    pub fn warning(
261        code: DwCode,
262        stage: impl Into<String>,
263        path: impl Into<String>,
264        message: impl Into<String>,
265    ) -> Self {
266        Diagnostic {
267            code: code.id().to_string(),
268            severity: Severity::Warning,
269            stage: stage.into(),
270            path: path.into(),
271            message: message.into(),
272            subject: code.subject(),
273        }
274    }
275
276    /// **Which of a run's three groups this line belongs in**, lowest first.
277    ///
278    /// The one authority on the order `delvec` prints in. See [`Subject`] for
279    /// what the split is and why.
280    #[must_use]
281    pub fn group(&self) -> Group {
282        match (self.severity, self.subject) {
283            (Severity::Error, _) => Group::Refusal,
284            (Severity::Warning, Subject::Campaign) => Group::AboutTheCampaign,
285            (Severity::Warning, Subject::Engine) => Group::AboutTheEngine,
286        }
287    }
288}
289
290/// **Whose state a code's verdict is about.**
291///
292/// The question this answers, and the only question it answers: *if the author
293/// changed nothing about their campaign and the engine's own tables were
294/// finished, would this line go away?*
295#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize)]
296pub enum Subject {
297    /// The campaign. Every refusal, and every advisory whose verdict is a fact
298    /// about the documents in front of the author — the default, because a
299    /// diagnostic is addressed to an author unless it says otherwise.
300    #[default]
301    Campaign,
302    /// **The ENGINE**, regardless of the campaign: an engine table that is still
303    /// seeded, a standard that has not been calibrated. Nothing the author can
304    /// write moves it, and it is identical on every campaign the engine
305    /// compiles, so it prints after the lines that ARE theirs — see [`Group`].
306    ///
307    /// This is not a licence to make a campaign's problem quiet. The test is
308    /// whether the line would read the same on a different campaign; where it
309    /// names something the author wrote, it is a [`Subject::Campaign`] verdict
310    /// however advisory its tier.
311    Engine,
312}
313
314/// **The order a run's diagnostics are printed in**, and the labels they are
315/// printed under.
316///
317/// Author-actionable first, then advisories about the campaign, then notices
318/// about the engine. Measured on every site-plan run before this existed: four
319/// to six paragraphs saying "this is fine" or "the engine's own table is
320/// provisional", ahead of the one line the author was there to act on.
321///
322/// Ordering only — nothing is dropped, and every code that reported before
323/// reports now.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
325pub enum Group {
326    /// A hard rejection. Yours to act on.
327    Refusal,
328    /// An advisory about the campaign: a measurement, or a verdict that depends
329    /// on something outside the documents.
330    AboutTheCampaign,
331    /// A notice about this engine, true regardless of the campaign.
332    AboutTheEngine,
333}
334
335impl Group {
336    /// The heading this group is printed under, with `n` lines in it.
337    #[must_use]
338    pub fn heading(self, n: usize) -> String {
339        match self {
340            Group::Refusal => format!("-- {n} refusal(s): these are yours to act on"),
341            Group::AboutTheCampaign => format!("-- {n} advisory(ies) about this campaign"),
342            Group::AboutTheEngine => {
343                format!("-- {n} notice(s) about this engine, true of any campaign")
344            }
345        }
346    }
347}
348
349#[doc(hidden)]
350pub use linkme as __linkme;
351
352/// **Every DW code this binary declares**, one entry per [`dw_code!`]
353/// declaration, gathered by the linker from wherever the declaration sits.
354///
355/// The declarations are the one authority: a code is declared by writing it
356/// inside `dw_code!`, and that same act registers it, so there is no list to
357/// keep beside them. `delvec codes` prints this registry, and
358/// `tools/ci/check-dw-codes.py --delvec` holds it equal to the declarations the
359/// source spells, in both directions.
360#[linkme::distributed_slice]
361pub static DECLARED: [Declared];
362
363/// One registered declaration: the code, the constant that names it, and the
364/// properties the code carries.
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
366pub struct Declared {
367    /// The code string (`DW0944`).
368    pub code: &'static str,
369    /// The exit tier, or `None` for a bare `&str` code — one raised by a verb
370    /// outside the campaign pipeline (`prefab`, `schem`, `render`, the view
371    /// arms), whose own exit table decides.
372    pub tier: Option<ExitTier>,
373    /// Whose state the verdict is about; `None` exactly when `tier` is.
374    pub subject: Option<Subject>,
375    /// The constant's name.
376    pub name: &'static str,
377    /// The module that declares it.
378    pub module: &'static str,
379}
380
381impl Declared {
382    #[doc(hidden)]
383    pub const fn coded(name: &'static str, module: &'static str, code: DwCode) -> Declared {
384        Declared {
385            code: code.id,
386            tier: Some(code.tier),
387            subject: Some(code.subject),
388            name,
389            module,
390        }
391    }
392
393    #[doc(hidden)]
394    pub const fn bare(name: &'static str, module: &'static str, code: &'static str) -> Declared {
395        Declared {
396            code,
397            tier: None,
398            subject: None,
399            name,
400            module,
401        }
402    }
403}
404
405/// The registry in a stable order — by code, then module, then name — because
406/// the linker's order is the link order and promises nothing.
407#[must_use]
408pub fn declared() -> Vec<Declared> {
409    let mut all: Vec<Declared> = DECLARED.iter().copied().collect();
410    all.sort_by(|a, b| (a.code, a.module, a.name).cmp(&(b.code, b.module, b.name)));
411    all
412}
413
414/// **Declare a DW code** — the only way to. Expands to the constant as written
415/// and registers it in [`DECLARED`]:
416///
417/// ```text
418/// dw_code! {
419///     /// What the rule refuses.
420///     pub const <NAME>: DwCode = DwCode::new("<code>", ExitTier::<tier>);
421/// }
422/// dw_code! { pub const <NAME>: &str = "<code>"; }
423/// ```
424///
425/// The declaration inside the braces is spelled the way
426/// `tools/ci/check-dw-codes.py`'s `CONST_RE` reads it, so the source reading
427/// and the binary's registry are two readings of one text.
428#[macro_export]
429macro_rules! dw_code {
430    (
431        $(#[$meta:meta])*
432        $vis:vis const $name:ident : DwCode = DwCode::new($id:literal, ExitTier::$tier:ident)
433            $(.$chain:ident())? ;
434    ) => {
435        $(#[$meta])*
436        // `DwCode` and `ExitTier` resolve where the declaration is written, as
437        // they did before it was wrapped: the declaring module imports them.
438        $vis const $name: DwCode = DwCode::new($id, ExitTier::$tier)$(.$chain())?;
439        const _: () = {
440            #[$crate::diagnostic::__linkme::distributed_slice($crate::diagnostic::DECLARED)]
441            #[linkme(crate = $crate::diagnostic::__linkme)]
442            static DECLARED_CODE: $crate::diagnostic::Declared =
443                $crate::diagnostic::Declared::coded(stringify!($name), module_path!(), $name);
444        };
445    };
446    (
447        $(#[$meta:meta])*
448        $vis:vis const $name:ident : &str = $id:literal ;
449    ) => {
450        $(#[$meta])*
451        $vis const $name: &str = $id;
452        const _: () = {
453            #[$crate::diagnostic::__linkme::distributed_slice($crate::diagnostic::DECLARED)]
454            #[linkme(crate = $crate::diagnostic::__linkme)]
455            static DECLARED_CODE: $crate::diagnostic::Declared =
456                $crate::diagnostic::Declared::bare(stringify!($name), module_path!(), $name);
457        };
458    };
459}
460
461/// The stable validation diagnostic codes (catalogued in
462/// `docs/reference/compiler.md` §5).
463///
464/// Every entry is a [`DwCode`], so every entry states its exit tier — there is
465/// no way to add one that does not.
466pub mod codes {
467    use super::{DwCode, ExitTier};
468
469    crate::dw_code! {
470        /// Document does not conform to its stage schema (unknown field / wrong type).
471        pub const SCHEMA: DwCode = DwCode::new("DW0100", ExitTier::Build);
472    }
473    crate::dw_code! {
474        /// Envelope `stage` does not match the document's slot.
475        pub const STAGE_MISMATCH: DwCode = DwCode::new("DW0101", ExitTier::Build);
476    }
477    crate::dw_code! {
478        /// Unsupported `dsl_version`.
479        pub const DSL_VERSION: DwCode = DwCode::new("DW0102", ExitTier::Build);
480    }
481    crate::dw_code! {
482        /// Inconsistent `campaign_id` across stages.
483        pub const CAMPAIGN_ID_MISMATCH: DwCode = DwCode::new("DW0103", ExitTier::Build);
484    }
485    crate::dw_code! {
486        /// Malformed id syntax (kebab-case / prefix).
487        pub const ID_SYNTAX: DwCode = DwCode::new("DW0110", ExitTier::Build);
488    }
489    crate::dw_code! {
490        /// Duplicate id within its namespace.
491        pub const ID_DUPLICATE: DwCode = DwCode::new("DW0111", ExitTier::Build);
492    }
493    crate::dw_code! {
494        /// Dangling reference: an id ref does not resolve.
495        pub const DANGLING_REF: DwCode = DwCode::new("DW0112", ExitTier::Build);
496    }
497    crate::dw_code! {
498        /// Stage-6 dialogue node unreachable from `root`.
499        pub const DIALOGUE_UNREACHABLE: DwCode = DwCode::new("DW0120", ExitTier::Build);
500    }
501    crate::dw_code! {
502        /// Stage-6 dialogue `root`/`next` references an unknown node.
503        pub const DIALOGUE_BAD_REF: DwCode = DwCode::new("DW0121", ExitTier::Build);
504    }
505    crate::dw_code! {
506        /// Stage-6 dialogue effect references an objective that is unknown, not a
507        /// `talk-to`, or a `talk-to` on a different NPC (foreign effect).
508        pub const DIALOGUE_BAD_OBJECTIVE: DwCode = DwCode::new("DW0122", ExitTier::Build);
509    }
510    crate::dw_code! {
511        /// A stage-5 `talk-to` objective has no reachable completing dialogue option
512        /// (the static half of the compiler's `DW0203` deadlock guarantee).
513        pub const DIALOGUE_UNCOVERED: DwCode = DwCode::new("DW0123", ExitTier::Build);
514    }
515    crate::dw_code! {
516        /// Quest dependency cycle.
517        pub const PLAN_CYCLE: DwCode = DwCode::new("DW0130", ExitTier::Build);
518    }
519    crate::dw_code! {
520        /// `finale` is not a declared quest.
521        pub const FINALE_UNKNOWN: DwCode = DwCode::new("DW0131", ExitTier::Build);
522    }
523    crate::dw_code! {
524        /// `finale` is not the convergent sink of the plan: some declared quest is
525        /// not a transitive dependency of it.
526        ///
527        /// **The name deliberately does not contain `FINALE_UNREACHABLE`, which
528        /// belongs to `DW0201`.** That code says the finale can never complete; this
529        /// one says nothing at all about the finale being reachable — in the fixture
530        /// that raises it the finale completes perfectly well and a side trip hangs
531        /// off the plan. Both are `DwCode`, so nothing but the name distinguishes
532        /// them at a call site, and `tools/ci/check-dw-codes.py` credits a bare
533        /// constant name mentioned in a crate's tests to **that crate's** code — so
534        /// one shared name would buy coverage for whichever rule the file happens to
535        /// sit next to.
536        pub const PLAN_NOT_CONVERGENT: DwCode = DwCode::new("DW0132", ExitTier::Build);
537    }
538    crate::dw_code! {
539        /// An optional quest inside the finale's dependency closure (spec-0051
540        /// §8.1) — including a finale that declares itself optional.
541        pub const OPTIONAL_ON_SPINE: DwCode = DwCode::new("DW0866", ExitTier::Build);
542    }
543    crate::dw_code! {
544        /// A mandatory quest whose `depends_on` edge or stage-5 `quest-complete`
545        /// trigger names an optional quest (spec-0051 §8.2).
546        pub const MANDATORY_ON_OPTIONAL: DwCode = DwCode::new("DW0867", ExitTier::Build);
547    }
548    crate::dw_code! {
549        /// A mandatory objective gated on a flag only an optional quest produces
550        /// (spec-0051 §8.3) — the mainline key behind participation.
551        ///
552        /// The participation-minimal replay (`DW0204`) is the compensating stronger
553        /// check behind it; this one refuses at the edge so the message can name
554        /// the strand.
555        pub const MAINLINE_KEY_OPTIONAL: DwCode = DwCode::new("DW0868", ExitTier::Build);
556    }
557    crate::dw_code! {
558        /// Objective `after` cycle.
559        pub const AFTER_CYCLE: DwCode = DwCode::new("DW0140", ExitTier::Build);
560    }
561    crate::dw_code! {
562        /// Anchor not provided by the area's bound prefab.
563        pub const ANCHOR_UNRESOLVED: DwCode = DwCode::new("DW0142", ExitTier::Build);
564    }
565    crate::dw_code! {
566        /// Item id not in the pinned 1.21.11 registry.
567        pub const ITEM_UNKNOWN: DwCode = DwCode::new("DW0143", ExitTier::Build);
568    }
569    crate::dw_code! {
570        /// (spec-0021) An `equipment` or `loot` enchantment id is not in the pinned
571        /// 1.21.11 enchantment registry.
572        pub const ENCHANTMENT_UNKNOWN: DwCode = DwCode::new("DW0433", ExitTier::Build);
573    }
574    crate::dw_code! {
575        /// (spec-0021) An enchantment level is outside the 1..=255 range vanilla's
576        /// `minecraft:enchantments` component can carry.
577        pub const ENCHANTMENT_LEVEL: DwCode = DwCode::new("DW0434", ExitTier::Build);
578    }
579    crate::dw_code! {
580        /// (spec-0021) Two `loot` entries target the same anchor, so one would
581        /// silently overwrite the other's contents.
582        pub const LOOT_DUPLICATE_ANCHOR: DwCode = DwCode::new("DW0435", ExitTier::Build);
583    }
584    crate::dw_code! {
585        /// (spec-0021) A `loot` declaration carries more stacks than the container
586        /// it fills has slots.
587        pub const LOOT_TOO_MANY_ITEMS: DwCode = DwCode::new("DW0432", ExitTier::Build);
588    }
589    crate::dw_code! {
590        /// A single-slot fill's `count` exceeds the item's `minecraft:max_stack_size`
591        /// in the pinned 1.21.11 registry. `item replace … container.<n> with <item>
592        /// <count>` fails **silently** above the cap, shipping an empty slot.
593        pub const ITEM_COUNT_OVER_STACK: DwCode = DwCode::new("DW0436", ExitTier::Build);
594    }
595    crate::dw_code! {
596        /// An `interact` declares `missing_item_hint` without a `requires_item`: the
597        /// hint answers a gate that does not exist, so it could never narrate.
598        pub const MISSING_ITEM_HINT_WITHOUT_ITEM: DwCode = DwCode::new("DW0437", ExitTier::Build);
599    }
600    crate::dw_code! {
601        /// Planned quest (stage 4) has no expansion in stage 5.
602        pub const QUEST_NOT_EXPANDED: DwCode = DwCode::new("DW0150", ExitTier::Build);
603    }
604    crate::dw_code! {
605        /// Stage-5 quest is not planned in stage 4.
606        pub const QUEST_NOT_PLANNED: DwCode = DwCode::new("DW0151", ExitTier::Build);
607    }
608    crate::dw_code! {
609        /// Stage-2 NPC has no stage-6 dialogue tree.
610        pub const NPC_WITHOUT_TREE: DwCode = DwCode::new("DW0152", ExitTier::Build);
611    }
612    crate::dw_code! {
613        /// Stage-6 dialogue tree references an NPC not declared in stage 2.
614        pub const TREE_WITHOUT_NPC: DwCode = DwCode::new("DW0153", ExitTier::Build);
615    }
616    crate::dw_code! {
617        /// Area binds neither or both of `prefab` / `prefab_pool` (exactly one
618        /// required).
619        pub const PREFAB_BINDING: DwCode = DwCode::new("DW0160", ExitTier::Build);
620    }
621    crate::dw_code! {
622        /// Area `prefab_pool` references a pool absent from `prefabs/` metadata.
623        pub const POOL_UNKNOWN: DwCode = DwCode::new("DW0161", ExitTier::Build);
624    }
625    crate::dw_code! {
626        /// Area `prefab` names a piece absent from `prefabs/` metadata — the same
627        /// obligation [`POOL_UNKNOWN`] carries on the other arm of the binding. It
628        /// is an error rather than a deferral because an area whose piece is absent
629        /// contributes no anchor set at all, so every per-area anchor proof over it
630        /// is SKIPPED rather than failed: a misspelling here is strictly less
631        /// checked than a correct name.
632        pub const PREFAB_UNKNOWN: DwCode = DwCode::new("DW0856", ExitTier::Build);
633    }
634    crate::dw_code! {
635        /// (v0.6, spec-0017) A stage-7 edit script is structurally invalid: an edit
636        /// names a region no earlier `select` in its batch defined, a composition
637        /// (`union`/`intersect`/`subtract`) lists too few regions, a box `min`
638        /// exceeds `max` on an axis, a surface band's `from` exceeds `to`, a palette
639        /// recipe is empty / carries a non-positive or non-finite weight or `scale`,
640        /// a `matching` list is empty, or a morph `by`/`passes` is 0. (Unknown block
641        /// ids in recipes reuse [`BLOCK_UNKNOWN`] / `DW0193`; id-syntax and
642        /// duplicate-name violations reuse `DW0110`/`DW0111`.)
643        pub const EDIT_INVALID: DwCode = DwCode::new("DW0162", ExitTier::Build);
644    }
645    crate::dw_code! {
646        /// (v0.3) A `kill` objective or `spawn-wave` effect references a `wave/<id>`
647        /// not declared in the stage-5 `waves` section (dangling wave reference).
648        pub const WAVE_UNKNOWN: DwCode = DwCode::new("DW0170", ExitTier::Build);
649    }
650    crate::dw_code! {
651        /// (v0.3) A declared wave is referenced by a `kill` objective but is never
652        /// spawned by any `spawn-wave` effect (referenced-but-never-spawned). A wave
653        /// must be spawned by some effect before its kill objective is reachable.
654        pub const WAVE_NEVER_SPAWNED: DwCode = DwCode::new("DW0171", ExitTier::Build);
655    }
656    crate::dw_code! {
657        /// (v0.3) A `requires_flags` entry references a `flag/<id>` that no `set-flag`
658        /// effect ever produces (dangling flag reference).
659        pub const FLAG_UNKNOWN: DwCode = DwCode::new("DW0172", ExitTier::Build);
660    }
661    crate::dw_code! {
662        /// (spec-0016 §1) A wave declares `respawns_on_rest: true` but the campaign
663        /// declares no `bonfire` — nothing can ever re-seat it, so the field is a
664        /// silent no-op. Either add the bonfire the re-seat is meant to hang off, or
665        /// drop the field.
666        pub const REST_RESEAT_NO_BONFIRE: DwCode = DwCode::new("DW0370", ExitTier::Build);
667    }
668    crate::dw_code! {
669        /// (spec-0016 §1) The campaign places a `bonfire`
670        /// but no class kit declares a `flask`. Resting replenishes the flask to its
671        /// declared count; with no flask the rest interaction's whole recovery half
672        /// is a no-op and the souls loop has no consumable to spend, so this is a
673        /// build error rather than a design choice.
674        pub const BONFIRE_NO_FLASK: DwCode = DwCode::new("DW0476", ExitTier::Build);
675    }
676    crate::dw_code! {
677        /// **An item gate a class cannot bring.** An objective completes only for a
678        /// player holding a named item, and some class's player has no way to be
679        /// holding it: the item's only source in the whole campaign is *another*
680        /// class's kit, or it has no source at all.
681        ///
682        /// A delve is played by one to four players who each pick one class, so an
683        /// objective reachable only by one class's pick is an objective a party can
684        /// be assembled unable to finish — and the party finds out at the thing they
685        /// cannot press. Quantified over EVERY class for the same reason
686        /// [`BONFIRE_NO_FLASK`] is: one class that cannot bring it is as broken as
687        /// none, because a solo player of that class is a supported party.
688        pub const ITEM_GATE_UNBRINGABLE: DwCode = DwCode::new("DW0849", ExitTier::Build);
689    }
690    crate::dw_code! {
691        /// (spec-0016 §1) A kit item's potion `contents`
692        /// is not something 1.21.11 can pour: declared on an item that carries no
693        /// `minecraft:potion_contents` component, empty (neither a named potion nor
694        /// an effect), an unknown potion or status-effect id, an amplifier or
695        /// duration outside the field vanilla stores it in, a lasting effect with no
696        /// `duration`, an instantaneous one *with* a duration, or a malformed
697        /// `color`.
698        pub const KIT_POTION_INVALID: DwCode = DwCode::new("DW0486", ExitTier::Build);
699    }
700    crate::dw_code! {
701        /// (spec-0016 §1) A potion-bearing kit item
702        /// declares no `contents` at `dsl_version` 0.8.0 — the Uncraftable Potion, a
703        /// bottle that pours nothing. The placeholder flask, as a build error.
704        pub const KIT_POTION_MISSING: DwCode = DwCode::new("DW0487", ExitTier::Build);
705    }
706    crate::dw_code! {
707        /// A `drops[]` `slot` entry does not
708        /// name a distinct slot the same entity's `equipment` actually fills — the
709        /// slot is empty, or the same slot is declared twice. A mob can only leave
710        /// behind a piece it wears, and it can only leave it behind once.
711        pub const DROP_SLOT_UNFILLED: DwCode = DwCode::new("DW0490", ExitTier::Build);
712    }
713    crate::dw_code! {
714        /// `drops[]` on an encounter that is
715        /// not billed `elite` or `boss`. Only a named fight leaves anything behind;
716        /// an ordinary mob's kit is never farmable (no-grind constitution), so the
717        /// declaration is refused rather than silently making rank-and-file gear
718        /// lootable.
719        pub const DROP_NOT_TIERED: DwCode = DwCode::new("DW0491", ExitTier::Build);
720    }
721    crate::dw_code! {
722        /// (spec-0067) An `equipment` piece is declared where the pinned game will
723        /// not show it on that body: the body's entity type draws no such piece in
724        /// that slot, the item declares a different slot, or the item's allowed
725        /// entities exclude the body. One code, three shapes in one message.
726        /// Validation-tier (exit 1).
727        pub const EQUIPMENT_UNSHOWN: DwCode = DwCode::new("DW0898", ExitTier::Build);
728    }
729    crate::dw_code! {
730        /// (spec-0071 §2) **A purchase whose literals do not add up.** In one effect
731        /// list, a charge (`add-state` moving a datum down) fires at a balance that
732        /// cannot pay it — the floor its own `when` and its list's enclosing gate
733        /// leave open is below the amount charged, or nothing floors it at all — or
734        /// the arm that answers below the price stops at a ceiling that is not one
735        /// below where the charge starts, leaving a gap or an overlap. A price has no
736        /// field (spec-0032): it is written as gate terms and a charge, so the
737        /// engine compares the copies. Validation-tier (exit 1).
738        pub const PURCHASE_ARITHMETIC: DwCode = DwCode::new("DW0901", ExitTier::Build);
739    }
740    crate::dw_code! {
741        /// (spec-0073 §8.1) **A health bar over a body whose health cannot move**:
742        /// an actor declaring `health_bar` that is not `vulnerable` and that no
743        /// `unleash-actor` names, so the only body the bar could ever read is an
744        /// invulnerable puppet. Validation-tier (exit 1).
745        pub const HEALTH_BAR_STILL: DwCode = DwCode::new("DW0909", ExitTier::Build);
746    }
747    crate::dw_code! {
748        /// (spec-0073 §8.2) **A health bar with nothing to title it**: no `title`
749        /// (or a blank one), and the fight has no single name of its own — a wave of
750        /// two entries or more, or a body with no `name`. Validation-tier (exit 1).
751        pub const HEALTH_BAR_UNTITLED: DwCode = DwCode::new("DW0910", ExitTier::Build);
752    }
753    crate::dw_code! {
754        /// (spec-0073 §8.4) **Advisory: a fight billed `boss` declares no
755        /// `health_bar`.** Warning tier, never blocking — the build proceeds. Fires
756        /// for `boss` only, on a wave or an actor alike; `elite` and `ordinary` are
757        /// never named by it.
758        pub const HEALTH_BAR_ADVISED: DwCode = DwCode::new("DW0912", ExitTier::Build);
759    }
760    crate::dw_code! {
761        /// (spec-0074 §8.1) **An `on_kill` bundle on a body no player can be credited
762        /// with killing**: on a wave no beat spawns (it resolves no area, so it has
763        /// no bodies and no kill machinery), or on an actor no `unleash-actor` names
764        /// that is not `vulnerable` (its body is `Invulnerable` for the whole delve).
765        /// A declaration nothing can exercise is refused. Validation-tier (exit 1).
766        pub const ON_KILL_UNREACHABLE: DwCode = DwCode::new("DW0913", ExitTier::Build);
767    }
768    crate::dw_code! {
769        /// (spec-0076 §7) **A standing display the sidebar cannot draw as declared.**
770        /// A `state[]` datum declares `display: sidebar` and the slot cannot show
771        /// what it is handed: a second datum already asks for the one slot (the
772        /// sidebar holds one objective, and "first wins" would hide a decision the
773        /// creator has to make); the datum has no `name` (the slot's heading is the
774        /// display name, and without one the objective's id would stand on screen);
775        /// or the datum is `party`-scoped (its value lives on `#party`, and the
776        /// sidebar hides every `#`-prefixed holder, so the display would be an empty
777        /// heading). One rule about what the slot can draw, three ways to ask for
778        /// what it cannot — the `DW0520` shape. Validation-tier (exit 1).
779        /// Prescription: keep one `display`, give the datum a `name`, or declare it
780        /// `player`-scoped; a `party` purse keeps its announcement and stands nowhere.
781        pub const STATE_DISPLAY_UNDRAWABLE: DwCode = DwCode::new("DW0919", ExitTier::Build);
782    }
783    crate::dw_code! {
784        /// (spec-0081 §6) **A celestial time whose shape states nothing a sky can
785        /// show.** One rule about one value's shape, four ways to break it: the
786        /// object names neither or both of `sun` / `moon`; it states a `phase` where
787        /// the moon is below the horizon; `world.time` states no `phase` where the
788        /// moon is up; a `set-time`, a design row or a camera states the phase the
789        /// world already declares. Validation-tier (exit 1). Prescription: name one
790        /// body, remove the phase nobody can see, state the phase the party sees, or
791        /// remove the restated phase.
792        pub const CELESTIAL_TIME: DwCode = DwCode::new("DW0931", ExitTier::Build);
793    }
794    crate::dw_code! {
795        /// (spec-0082 §5.1, §5.5) **An assembly's rig cannot be emitted as
796        /// declared.** The library holds no `rigs/<name>/rig.json` for the
797        /// assembly's `rig`, or the file does not parse, or it breaks a structural
798        /// rule (no part, an unknown block, a clip with no frame, a frame short a
799        /// part, a cadence outside `1..=20`, a non-finite transform, a zero scale);
800        /// or an `initial`, a strike step's `windup`/`strike`, or a `play-clip`
801        /// names a clip the rig lacks — the message lists the rig's clips.
802        /// Validation-tier (exit 1). Prescription: regenerate the rig with its
803        /// generator, or name a clip the rig declares (`delvec rig describe`
804        /// prints them).
805        pub const ASSEMBLY_RIG: DwCode = DwCode::new("DW0935", ExitTier::Build);
806    }
807    crate::dw_code! {
808        /// (spec-0094 §5.2) **A locked strike's blow is declared where the lock
809        /// derives it.** A locked step's blow lands on the cells its chosen
810        /// clip comes down on at the locked turn, so a `damage-players` at the
811        /// top of its `on_land` that declares an `in` box, any `damage-players`
812        /// nested inside another effect's list there (it cannot be moved with
813        /// the lock), and a locked step in a pattern that also declares `aim`
814        /// (two rules choosing one turn) are refused, each naming the field.
815        /// Validation-tier (exit 1). Prescription: drop the `in`, lift the
816        /// `damage-players` to the top of `on_land`, or drop `aim` or `lock`.
817        pub const ASSEMBLY_LOCK_SHAPE: DwCode = DwCode::new("DW0969", ExitTier::Build);
818    }
819    crate::dw_code! {
820        /// (spec-0094 §3.3) **An `arm-strikes` names an assembly that never
821        /// strikes.** The assembly declares no `strikes`, so there is no pattern
822        /// to re-arm and the beat does nothing. Validation-tier (exit 1).
823        /// Prescription: give the assembly a `strikes` pattern, or drop the
824        /// effect.
825        pub const ASSEMBLY_ARM_NOTHING: DwCode = DwCode::new("DW0970", ExitTier::Build);
826    }
827    crate::dw_code! {
828        /// A `collect` `dropped_by` is not backed by the wave it names:
829        /// the wave declares no `{item}` drop of this objective's item, the count
830        /// asks for more copies than the wave's mobs can yield, or the objective
831        /// also declares a `container` (the item cannot come out of a box *and* off
832        /// a body).
833        pub const DROP_COLLECT_UNSOURCED: DwCode = DwCode::new("DW0492", ExitTier::Build);
834    }
835    crate::dw_code! {
836        /// A `collect` `dropped_by` is not ordered after the fight that
837        /// produces it: no `kill` objective for that wave precedes this collect in
838        /// the objective graph. Without that edge "kill the boss, take its key" is
839        /// an authoring intention the quest graph cannot prove, and the collect
840        /// reads as reachable from the campaign's first tick.
841        pub const DROP_COLLECT_UNORDERED: DwCode = DwCode::new("DW0493", ExitTier::Build);
842    }
843    crate::dw_code! {
844        /// (spec-0031, DSL v0.10) A `lethal_volumes[]` entry's `message` is blank.
845        ///
846        /// The volume would still kill — and would kill in silence, which is the one
847        /// thing the declaration exists to prevent. There is no compiler default that
848        /// could be right for a cliff, a lava pit and an acid pool at once, so a blank
849        /// wording is refused rather than papered over: a gate that reports green
850        /// while the player learns nothing is exactly the vacuous pass CLAUDE.md names.
851        pub const LETHAL_MESSAGE_BLANK: DwCode = DwCode::new("DW0512", ExitTier::Build);
852    }
853    crate::dw_code! {
854        /// (spec-0086 §3.2, §3.4, §3.5) **A loop whose release is not a fact about the
855        /// party, or that has none.**
856        ///
857        /// One rule — *the gate is the release, and the release is the party's* —
858        /// asked four ways: a loop with no gate term at all (it holds forever, a
859        /// soft-lock spelled out); a `requires_state` term naming a `player`-scoped
860        /// datum (one player released and another looped is a party split in two);
861        /// a `counts` naming a `player`-scoped datum (for the same reason); and a
862        /// `teleport` inside `on_cross` (the body was just moved, and a second move in
863        /// the same tick is two carries with one position). Validation-tier (exit 1).
864        /// Prescription: a `party` datum, a flag, or a release the party reaches.
865        pub const LOOP_GATE: DwCode = DwCode::new("DW0949", ExitTier::Build);
866    }
867    crate::dw_code! {
868        /// (spec-0062) **A killing volume and what shows it disagree.**
869        ///
870        /// One rule, three shapes, and every remedy each names is admitted by the
871        /// others — which is why one code carries all of them (spec-0062 §4).
872        ///
873        /// * **Caught floor that shows nothing.** Some cell the party can walk to
874        ///   lies in the volume's keep-out, and the block under or in it is not one
875        ///   of the volume's `shown_by`. The player reads stone and dies on it. The
876        ///   remedy is the geometry's: lower the volume so its keep-out's top course
877        ///   lies under the floor, draw its `extent` in, or author a block vanilla
878        ///   hurts with under those cells and declare it. Never mark walkable-looking
879        ///   ground unwalkable — the compiler knows and the player does not.
880        /// * **A declared signal the bytes do not hold.** A `shown_by` block under
881        ///   or in no caught cell, whether the list is wrong or the volume catches
882        ///   nothing at all. The `DW0887` shape, on a volume instead of a waterline.
883        /// * **A `shown_by` naming a block vanilla does not hurt a body with.** The
884        ///   document arm, and the only one answerable with nothing placed:
885        ///   `minecraft:stone` over stone is borne out by the bytes and shows
886        ///   nothing.
887        ///
888        /// The world arm is raised by `delvec::compiler::lethal` over the final
889        /// assembled world (whether a cell is floor is a fact about the settled
890        /// bytes, so nothing before assembly can answer it); the document arm here,
891        /// by [`crate::validate`].
892        pub const LETHAL_INVISIBLE: DwCode = DwCode::new("DW0891", ExitTier::Build);
893    }
894    crate::dw_code! {
895        /// (spec-0088) **A gate that cannot stage a lethal volume.** Two shapes,
896        /// refused at the document where they are entered:
897        ///
898        /// * **`when: {}`** — a stage with no term. An always-live volume is
899        ///   spelled by leaving `when` out; an empty gate says nothing and is not
900        ///   a stage.
901        /// * **A `requires_state` term on a `player`-scoped datum.** A volume's
902        ///   liveness is a fact about the place, so its gate is a fact about the
903        ///   party: a term one player satisfies and another does not would be a
904        ///   pit that kills one body and spares the one beside it, and the sweep's
905        ///   entity half has no player to read a per-player score from.
906        ///
907        /// The remedy is to leave `when` out, or to name a flag or a `party`
908        /// datum. Raised by [`crate::validate`] with no world built.
909        pub const LETHAL_STAGE_GATE: DwCode = DwCode::new("DW0953", ExitTier::Build);
910    }
911    crate::dw_code! {
912        /// (spec-0031, DSL v0.10) **A grant whose removal is a later effect, not its
913        /// own duration.** A `give-effect` is still live at the moment a
914        /// `clear-effect` for the same effect fires in the same bundle, so the clear
915        /// — not the duration — is what ends it.
916        ///
917        /// A bundle that does not reach its end leaves the effect on the player: a
918        /// logout, a crash, a death mid-chain, a `sequence` whose remaining
919        /// `schedule` never runs. A duration expires with no cooperation from
920        /// anything, which is why `seconds` is mandatory and why vanilla's `infinite`
921        /// is absent from this surface — this diagnostic is what stops the same
922        /// hazard being rebuilt out of two effects that are individually fine.
923        pub const EFFECT_CLEARED_LIVE: DwCode = DwCode::new("DW0540", ExitTier::Build);
924    }
925    crate::dw_code! {
926        /// (spec-0031, DSL v0.10) A `give-effect`'s `seconds` is zero or beyond
927        /// [`crate::MAX_EFFECT_SECONDS`], or its `amplifier` is beyond
928        /// [`crate::MAX_POTION_AMPLIFIER`].
929        ///
930        /// Zero is the grant that never happens — the unbound-vacuity class as a
931        /// number. The ceilings are vanilla's own field widths, so a duration typed
932        /// in ticks or milliseconds is caught instead of silently overflowing.
933        pub const EFFECT_GRANT_BOUNDS: DwCode = DwCode::new("DW0541", ExitTier::Build);
934    }
935    crate::dw_code! {
936        /// (v0.3) A wave mob `entity` is not a known vanilla entity id. (Item-id
937        /// checks for `collect.item`, `interact.requires_item` and `give-item.item`
938        /// reuse [`ITEM_UNKNOWN`] / `DW0143`.)
939        pub const ENTITY_UNKNOWN: DwCode = DwCode::new("DW0173", ExitTier::Build);
940    }
941    crate::dw_code! {
942        /// (i18n) An l10n sidecar does not correctly cover a declared language: the
943        /// `l10n/<code>.json` file is absent, its envelope (`campaign_id` / `lang` /
944        /// `dsl_version`) is inconsistent, or it is **missing** a key from the
945        /// authoritative inventory (under-coverage). English (`en`) is implicit and
946        /// never declared, so it is never checked.
947        pub const L10N_MISSING: DwCode = DwCode::new("DW0180", ExitTier::Build);
948    }
949    crate::dw_code! {
950        /// (i18n) An l10n sidecar carries an **orphan** key that is not in the
951        /// authoritative string inventory derived from the stage docs (over-coverage).
952        pub const L10N_ORPHAN: DwCode = DwCode::new("DW0181", ExitTier::Build);
953    }
954    crate::dw_code! {
955        /// (i18n / harness oracle) A player-visible string — authored English or any
956        /// sidecar translation — contains the reserved completion-marker sigil
957        /// `[dw:complete`. That chat sequence is the validation bot's per-objective
958        /// completion oracle; content carrying it could forge a passing critical-path
959        /// step. The channel is reserved, not merely conventional.
960        pub const MARKER_RESERVED: DwCode = DwCode::new("DW0182", ExitTier::Build);
961    }
962    crate::dw_code! {
963        /// (i18n v2) A player-visible string — authored English or any sidecar
964        /// translation — contains a character from the reserved private-use block the
965        /// compiler uses to carry an l10n key from the stage docs to the text
966        /// component it is emitted into ([`crate::l10n::TR_SIGIL`]). Content carrying
967        /// it could impersonate a translation tag, or survive into the datapack and
968        /// render as a tofu box. The block is reserved, not merely conventional.
969        pub const TR_SIGIL_RESERVED: DwCode = DwCode::new("DW0183", ExitTier::Build);
970    }
971    crate::dw_code! {
972        /// (i18n v2) A declared language has no entry in the Minecraft language-code
973        /// mapping table ([`crate::l10n::mc_lang_code`]), so the resource pack has no
974        /// filename to write its `assets/delvewright/lang/<code>.json` under. A
975        /// language is never silently dropped: either the code is corrected to a
976        /// mapped one, or the table gains the entry.
977        pub const LANG_CODE_UNMAPPED: DwCode = DwCode::new("DW0184", ExitTier::Build);
978    }
979    crate::dw_code! {
980        /// (i18n v2) A campaign l10n sidecar defines a key in the reserved
981        /// `delvewright.` **chrome** namespace ([`crate::chrome`]). Those are the
982        /// engine's own on-screen strings — `New objective: `, `Choose your class`,
983        /// the default a bonfire shows — owned by the compiler, translated with it,
984        /// and authored by no campaign; a sidecar row under that prefix would be
985        /// written into the language file and silently replace product chrome for that
986        /// language. The namespace is reserved, not merely conventional.
987        pub const CHROME_RESERVED: DwCode = DwCode::new("DW0186", ExitTier::Build);
988    }
989    crate::dw_code! {
990        /// (i18n v2) An l10n sidecar row was translated from English the campaign no
991        /// longer holds: its `source` entry differs from the key's canonical English.
992        /// The translation is present, applied and **wrong**, and no key-set check can
993        /// see it — `DW0180`/`DW0181` compare key SETS, and a rewritten line moves no
994        /// key. Load-bearing for entity display names, whose key belongs to the first
995        /// site declaring a given text, so renaming one body can migrate a key to
996        /// another body and the row that goes stale is not the one the author edited.
997        pub const L10N_STALE: DwCode = DwCode::new("DW0187", ExitTier::Build);
998    }
999    crate::dw_code! {
1000        /// (i18n v2) An l10n sidecar records provenance for only some of its rows (or
1001        /// none), so `DW0187` cannot see the rest. A warning, not an error: the
1002        /// `source` map is additive, and this is the one-version deprecation window
1003        /// before it is required. It states the unguarded row count, so an
1004        /// unadopted sidecar is a reported number on every run rather than silence
1005        /// that reads like a pass.
1006        pub const L10N_PROVENANCE_MISSING: DwCode = DwCode::new("DW0188", ExitTier::Build);
1007    }
1008    crate::dw_code! {
1009        /// (v0.4, widened by spec-0084) An image id a campaign declares — a body's
1010        /// `skin.texture_id` or a `world.textures[]` row's `id` — is malformed (not a bare kebab
1011        /// token) or duplicated among the images of its kind (spec-0009). A missing `model` is a
1012        /// schema error (`DW0100`); a missing PNG is a build error (`DW0309`).
1013        pub const SKIN_INVALID: DwCode = DwCode::new("DW0190", ExitTier::Build);
1014    }
1015    crate::dw_code! {
1016        /// (v0.4) A `talk-to` objective has no **ungated** reachable completing
1017        /// dialogue option — every completing option is `requires_flags`-gated, so
1018        /// the objective can deadlock the moment it activates (spec-0008 §1). Keep at
1019        /// least one ungated completing path.
1020        pub const DIALOGUE_FLAG_DEADLOCK: DwCode = DwCode::new("DW0191", ExitTier::Build);
1021    }
1022    crate::dw_code! {
1023        /// (v0.4) A wave mob `effects[].effect` is not a known 1.21.11 effect id.
1024        pub const EFFECT_UNKNOWN: DwCode = DwCode::new("DW0192", ExitTier::Build);
1025    }
1026    crate::dw_code! {
1027        /// (v0.4) A `set-block` / `interact.prop` block id is not a known 1.21.11
1028        /// block id.
1029        pub const BLOCK_UNKNOWN: DwCode = DwCode::new("DW0193", ExitTier::Build);
1030    }
1031    crate::dw_code! {
1032        /// An `interact` objective's `prop` is a block a step fires — a pressure
1033        /// plate or the tripwire string ([`crate::stepped_blocks`]). The
1034        /// objective completes on a right-click, so the block invites an act that
1035        /// does nothing. A step is a `trigger` with `on: step`.
1036        pub const INTERACT_PROP_STEPPED: DwCode = DwCode::new("DW0957", ExitTier::Build);
1037    }
1038    crate::dw_code! {
1039        /// (v0.4) An environment trigger id is malformed (`DW0110`-style) or
1040        /// duplicated within the stage-5 `triggers` namespace.
1041        pub const TRIGGER_INVALID: DwCode = DwCode::new("DW0194", ExitTier::Build);
1042    }
1043    crate::dw_code! {
1044        /// (v0.4) A dialogue `talk-to` or `interact` objective targets an NPC after a
1045        /// `despawn-npc` removes it on a reachable path (spec-0008 §5).
1046        pub const NPC_DESPAWNED_REF: DwCode = DwCode::new("DW0195", ExitTier::Build);
1047    }
1048    crate::dw_code! {
1049        /// (v0.5) An area `lighting.min_light` is out of the 1..=14 range (spec-0010).
1050        pub const LIGHTING_RANGE: DwCode = DwCode::new("DW0196", ExitTier::Build);
1051    }
1052    crate::dw_code! {
1053        /// (v0.6) A stage-2 NPC declares `deferred: true` but **no** `spawn-npc` effect
1054        /// anywhere in the campaign ever summons it — the NPC never enters the world,
1055        /// so its dialogue tree and any `talk-to` on it are unreachable content. The
1056        /// NPC-lifecycle dual of [`NPC_DESPAWNED_REF`] / `DW0195`.
1057        ///
1058        /// (0197/0198 were *reserved* by spec-0011's draft and released when that spec
1059        /// renumbered to `DW0340`/`DW0341`; they were never emitted by any code.)
1060        pub const NPC_NEVER_SPAWNED: DwCode = DwCode::new("DW0197", ExitTier::Build);
1061    }
1062    crate::dw_code! {
1063        /// (v0.6) A `talk-to` on a `deferred` NPC activates before the NPC can exist:
1064        /// every `spawn-npc` for it sits in a quest that is a strict *descendant* of the
1065        /// objective's quest on the stage-4 DAG (and none fires from a trigger or
1066        /// dialogue), so the objective provably activates on an empty anchor.
1067        pub const NPC_SPAWNED_LATE: DwCode = DwCode::new("DW0198", ExitTier::Build);
1068    }
1069    crate::dw_code! {
1070        /// (v0.6) A `cutscene` effect's shape is invalid: it mixes the multi-shot
1071        /// `shots` list with the single-shot `path`/`seconds` fields, gives neither,
1072        /// or declares a shot with an empty camera `path`. A cutscene must resolve to
1073        /// at least one shot, and every shot to at least one camera position.
1074        pub const CUTSCENE_SHAPE: DwCode = DwCode::new("DW0199", ExitTier::Build);
1075    }
1076
1077    crate::dw_code! {
1078        /// (v0.6) `horizon: "ocean"` declared without a `boundary` (spec-0013):
1079        /// validation-tier (exit 1). An infinite swimmable sea with no return rule is
1080        /// an authoring error. Grouped in the DW032x world/region family by domain;
1081        /// unlike the compiler-tier DW030x geometry codes it is raised at DSL
1082        /// validation, so it exits 1.
1083        pub const OCEAN_NO_BOUNDARY: DwCode = DwCode::new("DW0320", ExitTier::Build);
1084    }
1085    crate::dw_code! {
1086        /// (v0.6) `boundary.margin` outside the `0..=64` range (spec-0013):
1087        /// validation-tier (exit 1).
1088        pub const BOUNDARY_MARGIN: DwCode = DwCode::new("DW0321", ExitTier::Build);
1089    }
1090    crate::dw_code! {
1091        /// A stage-1 `horizon` param is out of range, or is a param of a base other
1092        /// than the one declared (spec-0026): validation-tier (exit 1).
1093        pub const HORIZON_PARAM: DwCode = DwCode::new("DW0853", ExitTier::Build);
1094    }
1095    crate::dw_code! {
1096        /// A `horizon` whose base BUILDS terrain, on a campaign that states no
1097        /// extent for that terrain to stand around (spec-0026): validation-tier
1098        /// (exit 1).
1099        ///
1100        /// A surround rings a declared extent — a site plan's `region`. A campaign
1101        /// that seats its pieces with `areas[]` declares none, and the union of
1102        /// whatever gets placed is not a substitute: it is an artifact of the
1103        /// compiler's fixed area stride, mostly the void between areas, so ringing
1104        /// it builds a mountain range around empty space.
1105        pub const SURROUND_NO_REGION: DwCode = DwCode::new("DW0855", ExitTier::Build);
1106    }
1107    crate::dw_code! {
1108        /// (v0.6) A `sequence` effect is nested inside another `sequence` (directly, or
1109        /// reachable via a nested `move-actor` `on_arrive`) — timelines do not recurse
1110        /// (spec-0014). Flatten the inner steps into the outer timeline.
1111        pub const NESTED_SEQUENCE: DwCode = DwCode::new("DW0329", ExitTier::Build);
1112    }
1113
1114    crate::dw_code! {
1115        /// (v0.6) Trap declaration structurally invalid (spec-0011): a malformed or
1116        /// duplicated `trap/<id>`, an `at`/`disarm.via` that no area's prefab provides,
1117        /// or a trap whose `disarm.via` collides with its own trigger anchor.
1118        /// Validation-tier (exit 1). Renumbered off the spec's stale reserved number
1119        /// (0197 — since taken).
1120        pub const TRAP_INVALID: DwCode = DwCode::new("DW0340", ExitTier::Build);
1121    }
1122    crate::dw_code! {
1123        /// (spec-0016 §2) A `shortcut` declaration is structurally invalid: a
1124        /// malformed or duplicate `shortcut/<id>`, a `gate`/`unlock` anchor no area's
1125        /// prefab provides, or a `gate` that IS the `unlock` (the mechanism must sit
1126        /// on the far side, not in the doorway).
1127        pub const SHORTCUT_INVALID: DwCode = DwCode::new("DW0371", ExitTier::Build);
1128    }
1129    crate::dw_code! {
1130        /// (spec-0016 §2) A `close-gate` effect targets a gate a `shortcut` owns.
1131        /// A shortcut opens **permanently** — that is the whole pattern — so its
1132        /// permanence is structural: there is no verb that can put it back. Use a
1133        /// different gate for the point-of-no-return beat.
1134        pub const SHORTCUT_RESEALED: DwCode = DwCode::new("DW0372", ExitTier::Build);
1135    }
1136    crate::dw_code! {
1137        /// (spec-0016 §3) An `ambush` declaration is structurally invalid: a
1138        /// malformed or duplicate `ambush/<id>`, an empty `actors` list (an ambush
1139        /// that ambushes nobody), or the same actor listed twice (the second
1140        /// `spawn-actor` is a guarded no-op, so the author's intent silently halves).
1141        /// The telegraph is deliberately NOT required — an un-telegraphed ambush is
1142        /// core souls vocabulary.
1143        pub const AMBUSH_INVALID: DwCode = DwCode::new("DW0375", ExitTier::Build);
1144    }
1145    crate::dw_code! {
1146        /// (spec-0016 §4) A `timed-gate` declaration is structurally invalid: a
1147        /// malformed or duplicate `timed-gate/<id>`, an `open_ticks` or
1148        /// `closed_ticks` of 0 (a gate that never opens, or never closes — neither is
1149        /// a timing gate), a `phase` at or beyond the full cycle, or a gate another
1150        /// `timed-gate` or a `shortcut` already owns (two clocks fighting over one
1151        /// region, or a clock fighting a permanent open), or a `disarm.via` anchor no
1152        /// area's prefab provides / one that IS the gate anchor (the jam lever cannot
1153        /// live inside the span it stops).
1154        pub const TIMED_GATE_INVALID: DwCode = DwCode::new("DW0377", ExitTier::Build);
1155    }
1156    crate::dw_code! {
1157        /// A `close-gate` effect targets the gate of a `timed-gate` that
1158        /// declares a `disarm`. A disarm suppresses the clock **permanently with the
1159        /// gate resting open** — a jammed portcullis stays up — so, exactly like a
1160        /// `shortcut` (`DW0372`), its permanence is structural: there is no verb that
1161        /// can re-arm it. Use a different gate for the beat that must re-seal, or drop
1162        /// the `disarm`.
1163        pub const TIMED_GATE_REARMED: DwCode = DwCode::new("DW0389", ExitTier::Build);
1164    }
1165    crate::dw_code! {
1166        /// (spec-0016 §6) A wave's TD `lane` / `summon` declaration is structurally
1167        /// invalid or internally contradictory: an empty `waypoints` list, a
1168        /// waypoint anchor no area's prefab provides, a repeated consecutive
1169        /// waypoint, an `aggro_radius` outside `4..=64`, a mob whose
1170        /// `attributes.follow_range` disagrees with `aggro_radius` (they MUST be
1171        /// equal — a patrolling raider holds ground against a target it cannot
1172        /// engage), or `lane` together with `summon: aggro-edge` (a lane IS the
1173        /// routing; aggro-edge is its opposite).
1174        pub const LANE_INVALID: DwCode = DwCode::new("DW0381", ExitTier::Build);
1175    }
1176    crate::dw_code! {
1177        /// (spec-0016 §6) A lane wave contains a non-raider species. `Patrolling` /
1178        /// `patrol_target` are Raider NBT: on anything else they are dropped and the
1179        /// mob simply stands where it spawned. The admitted set is vanilla's own
1180        /// `#minecraft:raiders` tag, read from the vendored tag table — never a
1181        /// species list this engine keeps. Non-raiders use `summon: aggro-edge`
1182        /// instead.
1183        pub const LANE_NOT_RAIDER: DwCode = DwCode::new("DW0382", ExitTier::Build);
1184    }
1185    crate::dw_code! {
1186        /// (spec-0016 §6) A lane wave fields fewer than 2 mobs. A lone patroller
1187        /// sets `Patrolling:0b` on itself when it finds no companion within its
1188        /// follow range (vanilla), so a one-mob lane cancels itself.
1189        pub const LANE_SQUAD_TOO_SMALL: DwCode = DwCode::new("DW0383", ExitTier::Build);
1190    }
1191    crate::dw_code! {
1192        /// (spec-0016 §6) A lane `pillager` is not holding a crossbow. Its only
1193        /// attack goal is the crossbow goal, so a pillager that acquires a target it
1194        /// has no runnable attack for freezes in place indefinitely — patrol blocked
1195        /// by the target, nothing to run instead (live-verified deadlock).
1196        pub const LANE_UNARMED: DwCode = DwCode::new("DW0384", ExitTier::Build);
1197    }
1198    crate::dw_code! {
1199        /// (spec-0016 §6) A `summon: aggro-edge` wave mob declares no
1200        /// `attributes.follow_range`. That radius IS the summon ring — the distance
1201        /// at which the mob perceives the party — so it is authored, never guessed
1202        /// from a vanilla defaults table the compiler cannot verify.
1203        pub const AGGRO_EDGE_NO_RANGE: DwCode = DwCode::new("DW0385", ExitTier::Build);
1204    }
1205    crate::dw_code! {
1206        /// (v0.6) A trap dispense-payload item id is not in the pinned 1.21.11 registry
1207        /// (spec-0011; mirrors `DW0143`). Validation-tier (exit 1). Renumbered off the
1208        /// spec's stale reserved number (0198 — since taken).
1209        pub const TRAP_PAYLOAD_UNKNOWN: DwCode = DwCode::new("DW0341", ExitTier::Build);
1210    }
1211
1212    crate::dw_code! {
1213        /// (spec-0022) A trap declares **no consequence at all**: neither the legacy
1214        /// redstone `effect` nor a command `payload`. A trap that does nothing is
1215        /// mute hardware the completability proofs would nonetheless reason about,
1216        /// so it is a content mistake, not a no-op. Validation-tier (exit 1).
1217        pub const TRAP_NO_CONSEQUENCE: DwCode = DwCode::new("DW0440", ExitTier::Build);
1218    }
1219    crate::dw_code! {
1220        /// (spec-0022) A `volley` `projectile` / `collapse` `falling_block` /
1221        /// `then_floor` id is not in the pinned 1.21.11 registry (a `projectile`
1222        /// must be an ENTITY id, the collapse blocks BLOCK ids).
1223        /// Validation-tier (exit 1).
1224        pub const TRAP_VERB_ID_UNKNOWN: DwCode = DwCode::new("DW0441", ExitTier::Build);
1225    }
1226    crate::dw_code! {
1227        /// (spec-0022) A `volley`'s `salvos` / `interval` is out of range (`salvos`
1228        /// in `1..=16`, `interval` in `1..=200`). A volley fires its whole kill zone
1229        /// every salvo, so the entity count is `salvos x cells`; and salvos spread
1230        /// wider than the interval cap stop reading as one trap event.
1231        /// Validation-tier (exit 1).
1232        pub const VOLLEY_CADENCE: DwCode = DwCode::new("DW0443", ExitTier::Build);
1233    }
1234
1235    crate::dw_code! {
1236        /// (v0.6) A `shot_style` declaration is semantically invalid (spec-0015 shot
1237        /// grammar): a styled shot with no `subject`; style-only fields (`subject`,
1238        /// `subject_b`, `dist`, `degrees`, `bearing`) on an unstyled shot; a
1239        /// `subject_b` on a style other than `two-shot` (or a `two-shot` without
1240        /// one); `degrees` off `orbit-arc` or outside `45..=120`; `dist` outside
1241        /// `1..=48`; or `bearing` outside `-360..=360`. Validation-tier (exit 1).
1242        pub const SHOT_STYLE_INVALID: DwCode = DwCode::new("DW0348", ExitTier::Build);
1243    }
1244    crate::dw_code! {
1245        /// (v0.6) A `side-track` / `low-follow` shot whose subject has no
1246        /// compiler-known motion: those styles dolly *with* a moving subject, so the
1247        /// subject must be an NPC/actor with a matching `move-npc`/`move-actor` in
1248        /// the same effect group or the same `sequence` timeline (an `anchor`
1249        /// subject can never move). Validation-tier (exit 1). Use `locked-off` /
1250        /// `push-in` for a static subject instead.
1251        pub const SHOT_SUBJECT_UNMOVED: DwCode = DwCode::new("DW0349", ExitTier::Build);
1252    }
1253
1254    crate::dw_code! {
1255        /// (v0.4, added round-6) A `use` trigger anchored where an NPC stands.
1256        /// Right-click on an NPC already belongs to its dialogue advancement; a
1257        /// second interaction hitbox in the same cell makes the client's entity
1258        /// ray-pick ambiguous, and whichever entity loses the tie is silently dead
1259        /// — the round-6 island soft-lock class (an exactly co-located hitbox
1260        /// starved the giant's dialogue of every right-click). `strike` triggers
1261        /// are exempt: a left-click has no dialogue meaning, so the compiler rides
1262        /// the trigger's tag on the NPC's own hitbox instead of summoning a second
1263        /// one. Validation-tier (exit 1).
1264        pub const USE_TRIGGER_ON_NPC: DwCode = DwCode::new("DW0350", ExitTier::Build);
1265    }
1266
1267    crate::dw_code! {
1268        /// (v0.6, spec-0018) `world.min_players` outside the `1..=4` range. A delve is
1269        /// played by ONE party of 1–4 (ADR/CLAUDE.md product definition), so a declared
1270        /// mandatory party size can never sit outside it. Validation-tier (exit 1).
1271        pub const PARTY_SIZE: DwCode = DwCode::new("DW0356", ExitTier::Build);
1272    }
1273    crate::dw_code! {
1274        /// (spec-0077 §7) **A respawn wait that cannot be honoured.**
1275        /// `world.respawn_wait.seconds` lies outside `1..=120`, or the campaign
1276        /// declares a `respawn_wait` and no `set-checkpoint` or `bonfire` for a
1277        /// fallen player to come back to (the wait hangs off the checkpoint respawn
1278        /// edge, so with none it is a silently dead declaration). One rule about what
1279        /// a wait needs, two ways to break it. Validation-tier (exit 1). The build's
1280        /// own self-check that a shipped selector cannot read a waiter is `DW0926`.
1281        /// Prescription: a value in `1..=120`, or a checkpoint, or drop the field.
1282        pub const RESPAWN_WAIT_INVALID: DwCode = DwCode::new("DW0925", ExitTier::Build);
1283    }
1284    crate::dw_code! {
1285        /// (spec-0091) **A view aimed past the served view distance.** What a
1286        /// body is farther from than the served radius is never sent to its
1287        /// client, so a far view is a declaration (`world.view_distance`), and a
1288        /// thing aimed past it cannot render. Five shapes of one rule: a declared
1289        /// `view_distance` outside `FLOOR..=CEILING` (validation tier, exit 1,
1290        /// on `/content/view_distance`); a site-plan sightline or view longer
1291        /// than the served radius (validation tier, `crate::viewdistance`); a
1292        /// showcase camera whose subject — the first solid cell on its central
1293        /// ray, else where that ray enters the loaded scene — is beyond it, and
1294        /// a cutscene keyframe farther from its aim than it (both build tier,
1295        /// exit 3, against the assembled world). Prescription, in every shape:
1296        /// the fewest chunks that serve the distance, or the two ends nearer.
1297        pub const VIEW_BEYOND_SERVED: DwCode = DwCode::new("DW0956", ExitTier::Build);
1298    }
1299    crate::dw_code! {
1300        /// (v0.6, spec-0018) A `carrier: "one"` `give-item` sits in a bundle that is
1301        /// only ever reached from the **scheduler** (`move-npc`/`move-actor`
1302        /// `on_arrive`, a `sequence` step). `carrier: "one"` means "hand this single
1303        /// quest prop to the player whose action earned it"; a scheduled bundle runs
1304        /// with the server command source and has no acting player, so there is no
1305        /// defensible recipient. Give it to the whole party (drop `carrier`), or move
1306        /// the hand-off onto the beat that a player completes. Validation-tier (exit 1).
1307        pub const PARTY_CARRIER_SCHEDULED: DwCode = DwCode::new("DW0357", ExitTier::Build);
1308    }
1309
1310    crate::dw_code! {
1311        /// (v0.6) `world.difficulty` is `peaceful`. On
1312        /// peaceful the server discards every hostile-category mob as it is ticked —
1313        /// `/summon`ed, `NoAI`, `PersistenceRequired`, all of it — so a peaceful delve
1314        /// is one in which every wave, every hostile actor and every ambush silently
1315        /// ceases to exist. There is no delve that wants that, so the keyword is
1316        /// refused rather than honoured. Validation-tier (exit 1).
1317        pub const DIFFICULTY_INVALID: DwCode = DwCode::new("DW0468", ExitTier::Build);
1318    }
1319    crate::dw_code! {
1320        /// (v0.6) A campaign fields scripted `actors[]` (an
1321        /// ambush desugars into these too) but **no** `waves[]` and no declared
1322        /// `world.difficulty`, so the compiler's historical derivation ships
1323        /// `difficulty=peaceful` — under which every one of those actors that is a
1324        /// hostile species is discarded on the tick it spawns. The compiler cannot
1325        /// decide the question for the author: the pinned entity registry is a
1326        /// membership set with no mob-category data, so "is this actor a monster" is
1327        /// not something it can verify rather than guess. Advisory (warning,
1328        /// exit 0) — declaring `world.difficulty` settles it either way.
1329        pub const DIFFICULTY_UNDECLARED_ACTORS: DwCode = DwCode::new("DW0469", ExitTier::Build);
1330    }
1331    crate::dw_code! {
1332        /// (spec-0016 §1, spec-0023, souls ruling 5/7: "stage bosses never respawn
1333        /// on rest") A wave declares BOTH `tier: boss` and `respawns_on_rest: true`.
1334        /// `tier` and `respawns_on_rest` are two fields on the same [`Wave`]
1335        /// declaration — the only place a "boss" billing and a "re-seat on rest"
1336        /// contract can land on one another; an [`Actor`] carries `tier` too but has
1337        /// no `respawns_on_rest` field at all (it is killed by hand, never re-seated
1338        /// by a bonfire), so this is the sole structurally expressible violation of
1339        /// the ruling. A rest-respawning boss re-fight breaks the retry economy the
1340        /// ruling exists to protect: a boss is the campaign's named fight, not
1341        /// trash pressure the party grinds back down every rest. Validation-tier
1342        /// (exit 1), `dsl::validate`. Prescription: drop `tier: boss` if the
1343        /// encounter really is meant to re-seat (bill it `elite` instead), or drop
1344        /// `respawns_on_rest` if it really is the boss.
1345        ///
1346        /// [`Wave`]: crate::stages::Wave
1347        /// [`Actor`]: crate::stages::Actor
1348        pub const BOSS_RESPAWNS_ON_REST: DwCode = DwCode::new("DW0499", ExitTier::Build);
1349    }
1350
1351    // -- DSL v0.10 runtime state (spec-0031) ---------------------------------
1352
1353    crate::dw_code! {
1354        /// (v0.10, spec-0031) A `state/<kebab>` reference — in a `requires_state`
1355        /// comparison or in a `set-state`/`add-state`/`clear-state` verb — names a
1356        /// datum the campaign never declares in the stage-5 `state` list. Unlike a
1357        /// flag, a datum IS declared: its scope and its initial value are facts no
1358        /// use site can supply, so an undeclared reference is not "a datum that
1359        /// happens to start at zero", it is a datum with no defined multiplayer
1360        /// semantics at all. Validation-tier (exit 1). Prescription: declare it, or
1361        /// fix the id.
1362        pub const STATE_UNDECLARED: DwCode = DwCode::new("DW0500", ExitTier::Build);
1363    }
1364    crate::dw_code! {
1365        /// (v0.10, spec-0031) A gate's `requires_state` reads a declared datum that
1366        /// **no verb anywhere in the campaign ever writes**. The datum can only ever
1367        /// hold its declared `initial`, so the comparison's answer was decided at
1368        /// authoring time and the gate is a constant wearing a condition's clothes.
1369        ///
1370        /// This is the vacuity rule at the level of one datum (CLAUDE.md: *a green
1371        /// gate that binds to nothing is vacuous, not a pass*) — the numeric
1372        /// equivalent of the bot's combat floor examining zero enemies for nineteen
1373        /// rounds. Validation-tier (exit 1). Prescription: write the datum somewhere
1374        /// (`set-state`/`add-state`/`clear-state`), or drop the comparison and say
1375        /// what you meant unconditionally.
1376        pub const STATE_NEVER_WRITTEN: DwCode = DwCode::new("DW0501", ExitTier::Build);
1377    }
1378    crate::dw_code! {
1379        /// (v0.10, spec-0031) A declared datum that **no gate anywhere in the
1380        /// campaign ever reads**. Either some verb writes it and nothing ever asks
1381        /// (the write is inert — a counter nobody consults), or nothing touches it at
1382        /// all (a dead declaration). Runtime state exists to be compared against; a
1383        /// datum with no reader is bookkeeping no player can ever observe.
1384        /// Validation-tier (exit 1). Prescription: gate something on it with
1385        /// `requires_state`, or delete the declaration and its writes.
1386        pub const STATE_NEVER_READ: DwCode = DwCode::new("DW0502", ExitTier::Build);
1387    }
1388    crate::dw_code! {
1389        /// (v0.10, spec-0031) A `player`-scoped datum is referenced where emission
1390        /// has no acting player to read or write it against.
1391        ///
1392        /// Two such places exist, and both are properties of the SITE, not of the
1393        /// verb: a scheduler-only bundle (a `sequence` step, a `move-npc` /
1394        /// `move-actor` `on_arrive`) runs with the server command source — the same
1395        /// seam `DW0357` polices for `carrier: "one"` — and the gates emission
1396        /// evaluates against the party holder rather than against a player (an
1397        /// objective's activation guard, a trigger's arming gate, a trap's arming
1398        /// gate) have no `@s` either. Validation-tier (exit 1). Prescription: declare
1399        /// the datum `party`-scoped if the whole party shares it, or move the
1400        /// read/write onto a site a player drives (a dialogue option, a cast
1401        /// placement, an effect on a beat a player completes).
1402        pub const STATE_SCOPE_UNREACHABLE: DwCode = DwCode::new("DW0503", ExitTier::Build);
1403    }
1404    crate::dw_code! {
1405        /// (v0.10, spec-0032) A `stakes[]` declaration is unusable as a personal
1406        /// wager: its `state` is a datum the campaign never declares, or one declared
1407        /// `party`-scoped.
1408        ///
1409        /// **The scope half is the multiplayer decision most likely to be made by
1410        /// accident** (spec-0032, stated for correction rather than left to emerge).
1411        /// A stake is one player's loss and one player's chance to get it back; a
1412        /// party-shared purse would turn a teammate's death into a penalty on
1413        /// everyone, and nothing in the JSON would say so. Validation-tier (exit 1).
1414        /// Prescription: declare the datum `player`-scoped, or point the stake at a
1415        /// datum that is.
1416        pub const STAKE_STATE_SCOPE: DwCode = DwCode::new("DW0520", ExitTier::Build);
1417    }
1418    crate::dw_code! {
1419        /// (v0.10, spec-0032) A `drop-stake` effect names a stake the campaign never
1420        /// declares in the stage-5 `stakes` list. Validation-tier (exit 1).
1421        /// Prescription: declare it, or fix the id.
1422        pub const STAKE_UNDECLARED: DwCode = DwCode::new("DW0521", ExitTier::Build);
1423    }
1424    crate::dw_code! {
1425        /// (v0.10, spec-0032) A declared stake that **no `drop-stake` effect anywhere
1426        /// in the campaign ever leaves**. The retention policy, the forfeit rule and
1427        /// the whole placement table are computed for a mechanism no beat can fire —
1428        /// a declaration wearing a feature's clothes.
1429        ///
1430        /// The same vacuity rule `DW0502` states for a datum with no reader
1431        /// (CLAUDE.md: *a green gate that binds to nothing is vacuous, not a pass*).
1432        /// Validation-tier (exit 1). Prescription: drop it from a beat — `on_death`
1433        /// is the usual one — or delete the declaration.
1434        pub const STAKE_NEVER_DROPPED: DwCode = DwCode::new("DW0522", ExitTier::Build);
1435    }
1436    crate::dw_code! {
1437        /// (v0.10, spec-0032) A `shops[].offers[]` entry that cannot deliver
1438        /// anything: it declares no `effects`, so its button is drawn, is pressable,
1439        /// and does nothing.
1440        ///
1441        /// The shop analogue of the invisible-affordance rule: a control the player
1442        /// can operate must have an observable answer. A refusal counts — an offer
1443        /// whose only effect is a gated `narrate` saying "you cannot afford that" is
1444        /// exactly the authored shape spec-0032 asks for. Validation-tier (exit 1).
1445        /// Prescription: give the offer effects, or delete it.
1446        pub const SHOP_OFFER_INERT: DwCode = DwCode::new("DW0523", ExitTier::Build);
1447    }
1448    crate::dw_code! {
1449        /// (v0.10, spec-0032) A `forfeit` of kind `proportion` whose `percent` is
1450        /// above 100 — a death that takes more than the whole purse. Validation-tier
1451        /// (exit 1). Prescription: 0–100, or use `all`.
1452        pub const STAKE_FORFEIT_RANGE: DwCode = DwCode::new("DW0524", ExitTier::Build);
1453    }
1454    crate::dw_code! {
1455        /// (v0.10, spec-0032) **A comparison read after the bundle has already changed
1456        /// what it compares.** An effect's `requires_state` names a datum that an
1457        /// EARLIER effect in the same bundle writes, so the gate is evaluated against
1458        /// the post-write value, not the value the beat started with.
1459        ///
1460        /// Found in the emitted output of spec-0032's own first shop. The authored
1461        /// shape a shop wants is "the purchase behind `at-least 1`, the apology behind
1462        /// `at-most 0`" — and written in that order, buying your LAST ember prints both:
1463        /// the debit runs, the balance falls to 0, and the apology's gate — evaluated
1464        /// after it — now holds. Vanilla evaluates each `execute` when it reaches it,
1465        /// which is the whole reason a per-effect gate is useful, so this is not a bug
1466        /// to fix in emission: it is an ordering hazard that only reading the generated
1467        /// function reveals. The fix is always the same and always local — **put the
1468        /// reading effect before the writing one** — which is why this is a warning
1469        /// naming the earlier write rather than a refusal.
1470        ///
1471        /// Warning-tier (exit 0). Prescription: move the gated effect ahead of the
1472        /// write, or gate it on something the bundle does not itself change.
1473        pub const STATE_READ_AFTER_WRITE: DwCode = DwCode::new("DW0527", ExitTier::Build);
1474    }
1475
1476    crate::dw_code! {
1477        /// (v0.11) **A press answer addressed to a click vanilla cannot attribute.**
1478        /// A trigger declares `audience: presser` on something other than an
1479        /// `on: use` or an `on: step`.
1480        ///
1481        /// `minecraft:player_interacted_with_entity` is the only vanilla criterion
1482        /// that runs a function as the player who clicked, and it fires on
1483        /// right-clicks alone; a step is a player standing in the cell, which a
1484        /// positional selector names. A left-click is recorded in the interaction entity's
1485        /// `attack` NBT — a UUID no command can become — and an `approach` involves no
1486        /// click at all. Approximating it (polling the record and assuming the nearest
1487        /// player) is the downstream folklore CLAUDE.md's no-hack rule excludes, so the
1488        /// capability is refused rather than faked.
1489        pub const TRIGGER_AUDIENCE_UNATTRIBUTABLE: DwCode = DwCode::new("DW0427", ExitTier::Build);
1490    }
1491
1492    crate::dw_code! {
1493        /// (v0.11) **A trigger id in the compiler's reserved `dw-` namespace.** The
1494        /// compiler synthesizes triggers of its own — today the press answer every
1495        /// sealed gate and shortcut door gives (`trigger/dw-press-…`) — and two
1496        /// triggers sharing an id would share one `dw_trig_…` tag and one emitted
1497        /// function, so one of them would silently disappear. Reserving the prefix
1498        /// makes the collision impossible by construction instead of improbable.
1499        pub const TRIGGER_ID_RESERVED: DwCode = DwCode::new("DW0428", ExitTier::Build);
1500    }
1501
1502    crate::dw_code! {
1503        /// (v0.11) **A sealed body with no press answer**, uniformly over the
1504        /// pressable class. A `shortcuts[]` door or
1505        /// a `close-gate`'s wall is sealed, and nothing says what it answers when the
1506        /// party presses it — no `use` trigger anchored on it, and (for a
1507        /// `close-gate`) no authored `sealed_hint`.
1508        ///
1509        /// The compiler deliberately does **not** fill that silence. A baked default
1510        /// is the compiler making a design statement — about tone, about what this
1511        /// specific door is — on the author's behalf, and then never telling them it
1512        /// did; an error makes the author say it. Same rule as "no hacks at any
1513        /// layer": if content needs a thing, the DSL exposes it and the author
1514        /// declares it, rather than a lower layer inventing it.
1515        ///
1516        /// One rule for the whole pressable class: two objects of the same class do
1517        /// not get two defaulting policies, which would be the "capability keyed to
1518        /// the verb" defect this very surface is CLAUDE.md's worked example of.
1519        pub const SEALED_BODY_UNANSWERED: DwCode = DwCode::new("DW0429", ExitTier::Build);
1520    }
1521
1522    crate::dw_code! {
1523        /// (v0.11, spec-0034) **A declared locomotion the engine cannot hold the
1524        /// body to** — today exactly one value, `aquatic`.
1525        ///
1526        /// The declaration surface exists so an author can claim a capability and
1527        /// have the claim PROVEN. `aquatic` is the one
1528        /// class that carries no exemption and governs no rule: it is a ledger
1529        /// label the compiler derives from vanilla's own `#minecraft:aquatic` tag.
1530        /// Declaring it could therefore never change a verdict, so it would always
1531        /// land in `DW0454` — and a value whose only possible outcome is another
1532        /// diagnostic is a trap, not a surface.
1533        ///
1534        /// The gap it names, stated rather than left to folklore (CLAUDE.md's
1535        /// no-hack rule): the compiler routes **every** body on standable ground,
1536        /// and `flooded` cells are impassable and never floor for every body. There
1537        /// is no water-traversal model for a declaration to feed, so there is
1538        /// nothing to hold an aquatic claim to. When routing grows one, this
1539        /// refusal is what has to be deleted to enable the value.
1540        ///
1541        /// Error tier, raised in `validate_campaign_with`, so the run ends at the
1542        /// validation tier (exit 1). Prescription: remove the declaration — a body whose
1543        /// route crosses water is governed by the flooded-cell rules already, and
1544        /// the derived aquatic class still reaches the binding ledger.
1545        pub const TRAVERSAL_UNPROVABLE: DwCode = DwCode::new("DW0455", ExitTier::Build);
1546    }
1547
1548    crate::dw_code! {
1549        /// A gate contradicts itself, so it can NEVER open: a flag on both its
1550        /// `requires_flags` and `forbids_flags`, or `requires_state` terms on one
1551        /// datum that no integer satisfies (`at-least 5` with `at-most 3`, two
1552        /// different `equals`). The thing carrying it — objective, effect, trigger,
1553        /// trap, dialogue option, cast placement, shop offer — is authored content
1554        /// that provably never happens, which is a defect in what the document
1555        /// SAYS, not a stylistic lint. One rule over the whole closed consumer set
1556        /// ([`crate::gate::for_each_gate`]), because satisfiability is a property
1557        /// of the gate, never of the verb that first needed the question answered.
1558        ///
1559        /// Error tier, validation (exit 1).
1560        pub const GATE_NEVER_OPENS: DwCode = DwCode::new("DW0847", ExitTier::Build);
1561    }
1562    crate::dw_code! {
1563        /// An asset's licence is outside the ADR-0013 allowlist, or its record lacks
1564        /// a field the allowlist's rule for that asset requires. One code for every
1565        /// asset that records a licence: a prefab catalog card (`delvec prefab`,
1566        /// `delvec::admit::diag::DW_LICENSE`) and an image a campaign declares in
1567        /// `world.textures[]` (spec-0084 §6.3, `crate::license`).
1568        pub const LICENSE_REFUSED: DwCode = DwCode::new("DW0741", ExitTier::Build);
1569    }
1570    crate::dw_code! {
1571        /// (spec-0084 §6.1) A `world.textures[]` row's `replaces` names a texture the
1572        /// pinned client does not ship — not the `minecraft` namespace, a path with
1573        /// `textures/` or `.png` left on, another version's path, a misspelling — or
1574        /// two rows replace one texture. Judged against the census vendored from the
1575        /// pinned client jar (`delvec::compiler::textures`); the duplicate half is
1576        /// judged in validation, where no census is needed.
1577        pub const TEXTURE_PATH: DwCode = DwCode::new("DW0939", ExitTier::Build);
1578    }
1579    crate::dw_code! {
1580        /// (spec-0084 §6.2) A `world.textures[]` row's file is not an image the
1581        /// named texture can be replaced by: not a PNG, not `k·w₀ × k·h₀` of the
1582        /// vanilla frame (or `k·w₀ × n·k·h₀` with a sidecar), a sidecar that does not
1583        /// parse as vanilla's animation metadata, or bytes identical to vanilla's.
1584        pub const TEXTURE_IMAGE: DwCode = DwCode::new("DW0940", ExitTier::Build);
1585    }
1586    crate::dw_code! {
1587        /// (spec-0085 §4.3) **A particle the game does not draw from a bare id.** A
1588        /// `particle` effect names an id the pinned registry
1589        /// (`crates/dsl/data/particles-1.21.11.json`) does not hold, or one whose
1590        /// type takes options (`dust`, `block`, `item`, …) — which the verb cannot
1591        /// carry, so the emitted command would be refused by the game.
1592        ///
1593        /// Error tier, validation (exit 1).
1594        pub const PERCEPTION_UNKNOWN_PARTICLE: DwCode = DwCode::new("DW0941", ExitTier::Build);
1595    }
1596    crate::dw_code! {
1597        /// (spec-0085 §3.3) **An audience on a party fact.** An effect states the
1598        /// envelope's `audience` or `in` on a verb the emitter fires once for the
1599        /// world ([`crate::stages::Verb::addresses_players`] answers `false`) — a
1600        /// flag, a gate, a block, a region, a timeline, a rocket. A box has no party
1601        /// and a world fact has no audience; a `sequence`'s steps each state their
1602        /// own.
1603        ///
1604        /// Error tier, validation (exit 1).
1605        pub const PERCEPTION_AUDIENCE_ON_A_PARTY_FACT: DwCode = DwCode::new("DW0942", ExitTier::Build);
1606    }
1607    crate::dw_code! {
1608        /// (spec-0085 §5.3) **A sight effect that ends under a camera.** In one
1609        /// timeline, a `give-effect` of a sight effect
1610        /// ([`crate::perception::SIGHT`]) overlaps a `cutscene` step and ends inside
1611        /// it, or within the effect's wind-down after it, so the effect starts
1612        /// ramping down on screen. The `give-effect` half of the findings-ledger
1613        /// row whose general form is that a granted sight effect outlasts any
1614        /// authored camera it can overlap.
1615        ///
1616        /// Error tier, validation (exit 1).
1617        pub const PERCEPTION_SIGHT_UNDER_A_CAMERA: DwCode = DwCode::new("DW0944", ExitTier::Build);
1618    }
1619}
1620
1621#[cfg(test)]
1622mod tests {
1623    use super::*;
1624
1625    /// The tier's arithmetic is the CLI's published contract
1626    /// (`docs/reference/compiler.md` §1), so it is asserted rather than left to
1627    /// whoever next reads the `match`.
1628    #[test]
1629    fn a_tier_maps_to_its_published_exit_status() {
1630        assert_eq!(ExitTier::Analysis.exit_status(), 2);
1631        assert_eq!(ExitTier::Build.exit_status(), 3);
1632    }
1633
1634    /// A code carries the tier it was declared with, independently of what its
1635    /// number happens to spell — which is the
1636    /// whole point of moving the tier off the code's spelling. `DW0312` is the
1637    /// live instance: a `DW03xx` number that exits 2.
1638    #[test]
1639    fn a_code_carries_the_tier_it_declares_not_the_one_its_number_spells() {
1640        // `let`, not `const`: a `const NAME: DwCode = …` here would be a SECOND
1641        // diagnostic constant declaring a live code, and `tools/ci/check-dw-codes.py`
1642        // reads every `crates/**/*.rs` — it refuses one code declared twice, and
1643        // it is right to. Measured: this test written with `const` reds that gate
1644        // on all three codes.
1645        let analysis_spelt_dw03 = DwCode::new("DW0312", ExitTier::Analysis);
1646        let build_spelt_dw03 = DwCode::new("DW0311", ExitTier::Build);
1647
1648        assert_eq!(analysis_spelt_dw03.exit_tier(), ExitTier::Analysis);
1649        assert_eq!(analysis_spelt_dw03.exit_tier().exit_status(), 2);
1650        assert_eq!(build_spelt_dw03.exit_tier().exit_status(), 3);
1651    }
1652
1653    /// A code's properties are independent, and `about_the_engine`
1654    /// rebuilds the struct field by field — the one place where setting one
1655    /// could silently reset another. Today it cannot (there is no `Default` and
1656    /// no struct-update syntax, so an omitted field is a compile error), but
1657    /// "the compiler would catch it" is a claim about the current shape, and
1658    /// this is the assertion that survives the shape changing.
1659    #[test]
1660    fn marking_a_code_an_engine_notice_keeps_its_tier() {
1661        let engine_notice = DwCode::new("DW0813", ExitTier::Analysis).about_the_engine();
1662        assert_eq!(engine_notice.subject(), Subject::Engine);
1663        assert_eq!(engine_notice.exit_tier(), ExitTier::Analysis);
1664        assert_eq!(engine_notice.id(), "DW0813");
1665    }
1666}