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: [`crate::quest::check::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::refused_upstream` on every stage-6 verdict measured
33//!    against a seam the site plan wrote and did not resolve.
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        /// Unsupported `dsl_version`.
475        pub const DSL_VERSION: DwCode = DwCode::new("DW0102", ExitTier::Build);
476    }
477    crate::dw_code! {
478        /// Malformed id syntax (kebab-case / prefix).
479        pub const ID_SYNTAX: DwCode = DwCode::new("DW0110", ExitTier::Build);
480    }
481    crate::dw_code! {
482        /// Duplicate id within its namespace.
483        pub const ID_DUPLICATE: DwCode = DwCode::new("DW0111", ExitTier::Build);
484    }
485    crate::dw_code! {
486        /// Dangling reference: an id ref does not resolve.
487        pub const DANGLING_REF: DwCode = DwCode::new("DW0112", ExitTier::Build);
488    }
489    crate::dw_code! {
490        /// Anchor not provided by the area's bound prefab.
491        pub const ANCHOR_UNRESOLVED: DwCode = DwCode::new("DW0142", ExitTier::Build);
492    }
493    crate::dw_code! {
494        /// Item id not in the pinned 1.21.11 registry.
495        pub const ITEM_UNKNOWN: DwCode = DwCode::new("DW0143", ExitTier::Build);
496    }
497    crate::dw_code! {
498        /// (spec-0021) A `loot` declaration carries more stacks than the container
499        /// it fills has slots.
500        pub const LOOT_TOO_MANY_ITEMS: DwCode = DwCode::new("DW0432", ExitTier::Build);
501    }
502    crate::dw_code! {
503        /// (v0.3) A `kill` objective or `spawn-wave` effect references a `wave/<id>`
504        /// not declared in the stage-5 `waves` section (dangling wave reference).
505        pub const WAVE_UNKNOWN: DwCode = DwCode::new("DW0170", ExitTier::Build);
506    }
507    crate::dw_code! {
508        /// (v0.3) A `requires_flags` entry references a `flag/<id>` that no `set-flag`
509        /// effect ever produces (dangling flag reference).
510        pub const FLAG_UNKNOWN: DwCode = DwCode::new("DW0172", ExitTier::Build);
511    }
512    crate::dw_code! {
513        /// (spec-0067) An `equipment` piece is declared where the pinned game will
514        /// not show it on that body: the body's entity type draws no such piece in
515        /// that slot, the item declares a different slot, or the item's allowed
516        /// entities exclude the body. One code, three shapes in one message.
517        /// Validation-tier (exit 1).
518        pub const EQUIPMENT_UNSHOWN: DwCode = DwCode::new("DW0898", ExitTier::Build);
519    }
520    crate::dw_code! {
521        /// (spec-0073 §8.1) **A health bar over a body whose health cannot move**:
522        /// an actor declaring `health_bar` that is not `vulnerable` and that no
523        /// `unleash-actor` names, so the only body the bar could ever read is an
524        /// invulnerable puppet. Validation-tier (exit 1).
525        pub const HEALTH_BAR_STILL: DwCode = DwCode::new("DW0909", ExitTier::Build);
526    }
527    crate::dw_code! {
528        /// (spec-0073 §8.2) **A health bar with nothing to title it**: no `title`
529        /// (or a blank one), and the fight has no single name of its own — a wave of
530        /// two entries or more, or a body with no `name`. Validation-tier (exit 1).
531        pub const HEALTH_BAR_UNTITLED: DwCode = DwCode::new("DW0910", ExitTier::Build);
532    }
533    crate::dw_code! {
534        /// (spec-0073 §8.4) **Advisory: a fight billed `boss` declares no
535        /// `health_bar`.** Warning tier, never blocking — the build proceeds. Fires
536        /// for `boss` only, on a wave or an actor alike; `elite` and `ordinary` are
537        /// never named by it.
538        pub const HEALTH_BAR_ADVISED: DwCode = DwCode::new("DW0912", ExitTier::Build);
539    }
540    crate::dw_code! {
541        /// (spec-0074 §8.1) **An `on_kill` bundle on a body no player can be credited
542        /// with killing**: on a wave no beat spawns (it resolves no area, so it has
543        /// no bodies and no kill machinery), or on an actor no `unleash-actor` names
544        /// that is not `vulnerable` (its body is `Invulnerable` for the whole delve).
545        /// A declaration nothing can exercise is refused. Validation-tier (exit 1).
546        pub const ON_KILL_UNREACHABLE: DwCode = DwCode::new("DW0913", ExitTier::Build);
547    }
548    crate::dw_code! {
549        /// (spec-0081 §6) **A celestial time whose shape states nothing a sky can
550        /// show.** One rule about one value's shape, four ways to break it: the
551        /// object names neither or both of `sun` / `moon`; it states a `phase` where
552        /// the moon is below the horizon; `world.time` states no `phase` where the
553        /// moon is up; a `set-time`, a design row or a camera states the phase the
554        /// world already declares. Validation-tier (exit 1). Prescription: name one
555        /// body, remove the phase nobody can see, state the phase the party sees, or
556        /// remove the restated phase.
557        pub const CELESTIAL_TIME: DwCode = DwCode::new("DW0931", ExitTier::Build);
558    }
559    crate::dw_code! {
560        /// (spec-0088) **A gate that cannot stage a lethal volume.** Two shapes,
561        /// refused at the document where they are entered:
562        ///
563        /// * **`when: {}`** — a stage with no term. An always-live volume is
564        ///   spelled by leaving `when` out; an empty gate says nothing and is not
565        ///   a stage.
566        /// * **A `requires_state` term on a `player`-scoped datum.** A volume's
567        ///   liveness is a fact about the place, so its gate is a fact about the
568        ///   party: a term one player satisfies and another does not would be a
569        ///   pit that kills one body and spares the one beside it, and the sweep's
570        ///   entity half has no player to read a per-player score from.
571        ///
572        /// The remedy is to leave `when` out, or to name a flag or a `party`
573        /// datum. Raised by [`crate::validate`] with no world built.
574        pub const LETHAL_STAGE_GATE: DwCode = DwCode::new("DW0953", ExitTier::Build);
575    }
576    crate::dw_code! {
577        /// (v0.3) A wave mob `entity` is not a known vanilla entity id. (Item-id
578        /// checks for `collect.item`, `interact.requires_item` and `give-item.item`
579        /// reuse [`ITEM_UNKNOWN`] / `DW0143`.)
580        pub const ENTITY_UNKNOWN: DwCode = DwCode::new("DW0173", ExitTier::Build);
581    }
582    crate::dw_code! {
583        /// (i18n) An l10n sidecar does not correctly cover a declared language: the
584        /// `l10n/<code>.json` file is absent, its envelope (`campaign_id` / `lang` /
585        /// `dsl_version`) is inconsistent, or it is **missing** a key from the
586        /// authoritative inventory (under-coverage). English (`en`) is implicit and
587        /// never declared, so it is never checked.
588        pub const L10N_MISSING: DwCode = DwCode::new("DW0180", ExitTier::Build);
589    }
590    crate::dw_code! {
591        /// (i18n) An l10n sidecar carries an **orphan** key that is not in the
592        /// authoritative string inventory derived from the stage docs (over-coverage).
593        pub const L10N_ORPHAN: DwCode = DwCode::new("DW0181", ExitTier::Build);
594    }
595    crate::dw_code! {
596        /// (i18n / harness oracle) A player-visible string — authored English or any
597        /// sidecar translation — contains the reserved completion-marker sigil
598        /// `[dw:complete`. That chat sequence is the validation bot's per-objective
599        /// completion oracle; content carrying it could forge a passing critical-path
600        /// step. The channel is reserved, not merely conventional.
601        pub const MARKER_RESERVED: DwCode = DwCode::new("DW0182", ExitTier::Build);
602    }
603    crate::dw_code! {
604        /// (i18n v2) A player-visible string — authored English or any sidecar
605        /// translation — contains a character from the reserved private-use block the
606        /// compiler uses to carry an l10n key from the stage docs to the text
607        /// component it is emitted into ([`crate::l10n::TR_SIGIL`]). Content carrying
608        /// it could impersonate a translation tag, or survive into the datapack and
609        /// render as a tofu box. The block is reserved, not merely conventional.
610        pub const TR_SIGIL_RESERVED: DwCode = DwCode::new("DW0183", ExitTier::Build);
611    }
612    crate::dw_code! {
613        /// (i18n v2) A declared language has no entry in the Minecraft language-code
614        /// mapping table ([`crate::l10n::mc_lang_code`]), so the resource pack has no
615        /// filename to write its `assets/delvewright/lang/<code>.json` under. A
616        /// language is never silently dropped: either the code is corrected to a
617        /// mapped one, or the table gains the entry.
618        pub const LANG_CODE_UNMAPPED: DwCode = DwCode::new("DW0184", ExitTier::Build);
619    }
620    crate::dw_code! {
621        /// (i18n v2) A campaign l10n sidecar defines a key in the reserved
622        /// `delvewright.` **chrome** namespace ([`crate::chrome`]). Those are the
623        /// engine's own on-screen strings — `New objective: `, `Choose your class`,
624        /// the default a bonfire shows — owned by the compiler, translated with it,
625        /// and authored by no campaign; a sidecar row under that prefix would be
626        /// written into the language file and silently replace product chrome for that
627        /// language. The namespace is reserved, not merely conventional.
628        pub const CHROME_RESERVED: DwCode = DwCode::new("DW0186", ExitTier::Build);
629    }
630    crate::dw_code! {
631        /// (i18n v2) An l10n sidecar row was translated from English the campaign no
632        /// longer holds: its `source` entry differs from the key's canonical English.
633        /// The translation is present, applied and **wrong**, and no key-set check can
634        /// see it — `DW0180`/`DW0181` compare key SETS, and a rewritten line moves no
635        /// key. Load-bearing for entity display names, whose key belongs to the first
636        /// site declaring a given text, so renaming one body can migrate a key to
637        /// another body and the row that goes stale is not the one the author edited.
638        pub const L10N_STALE: DwCode = DwCode::new("DW0187", ExitTier::Build);
639    }
640    crate::dw_code! {
641        /// (i18n v2) An l10n sidecar records provenance for only some of its rows (or
642        /// none), so `DW0187` cannot see the rest. A warning, not an error: the
643        /// `source` map is additive, and this is the one-version deprecation window
644        /// before it is required. It states the unguarded row count, so an
645        /// unadopted sidecar is a reported number on every run rather than silence
646        /// that reads like a pass.
647        pub const L10N_PROVENANCE_MISSING: DwCode = DwCode::new("DW0188", ExitTier::Build);
648    }
649    crate::dw_code! {
650        /// (v0.4, widened by spec-0084, narrowed by spec-0097) An image id a campaign
651        /// declares is malformed (not a bare kebab token) — a body's `skin.texture_id`
652        /// or a `world.textures[]` row's `id` — or a `world.textures[]` row's `id` is
653        /// duplicated. A skin's `texture_id` names a file two bodies may both wear. A missing `model` is a
654        /// schema error (`DW0100`); a missing PNG is a build error (`DW0309`).
655        pub const SKIN_INVALID: DwCode = DwCode::new("DW0190", ExitTier::Build);
656    }
657    crate::dw_code! {
658        /// (v0.4) A wave mob `effects[].effect` is not a known 1.21.11 effect id.
659        pub const EFFECT_UNKNOWN: DwCode = DwCode::new("DW0192", ExitTier::Build);
660    }
661    crate::dw_code! {
662        /// (v0.4) A `set-block` / `interact.prop` block id is not a known 1.21.11
663        /// block id.
664        pub const BLOCK_UNKNOWN: DwCode = DwCode::new("DW0193", ExitTier::Build);
665    }
666    crate::dw_code! {
667        /// (v0.5) An area `lighting.min_light` is out of the 1..=14 range (spec-0010).
668        pub const LIGHTING_RANGE: DwCode = DwCode::new("DW0196", ExitTier::Build);
669    }
670
671    crate::dw_code! {
672        /// (spec-0091) **A view aimed past the served view distance.** What a
673        /// body is farther from than the served radius is never sent to its
674        /// client, so a far view is a declaration (`world.view_distance`), and a
675        /// thing aimed past it cannot render. Five shapes of one rule: a declared
676        /// `view_distance` outside `FLOOR..=CEILING` (validation tier, exit 1,
677        /// on `/content/view_distance`); a site-plan sightline or view longer
678        /// than the served radius (validation tier, `crate::viewdistance`); a
679        /// showcase camera whose subject — the first solid cell on its central
680        /// ray, else where that ray enters the loaded scene — is beyond it, and
681        /// a cutscene keyframe farther from its aim than it (both build tier,
682        /// exit 3, against the assembled world). Prescription, in every shape:
683        /// the fewest chunks that serve the distance, or the two ends nearer.
684        pub const VIEW_BEYOND_SERVED: DwCode = DwCode::new("DW0956", ExitTier::Build);
685    }
686
687    // -- DSL v0.10 runtime state (spec-0031) ---------------------------------
688
689    crate::dw_code! {
690        /// (v0.10, spec-0031) A `state/<kebab>` reference — in a `requires_state`
691        /// comparison or in a `set-state`/`add-state`/`clear-state` verb — names a
692        /// datum the campaign never declares in the stage-5 `state` list. Unlike a
693        /// flag, a datum IS declared: its scope and its initial value are facts no
694        /// use site can supply, so an undeclared reference is not "a datum that
695        /// happens to start at zero", it is a datum with no defined multiplayer
696        /// semantics at all. Validation-tier (exit 1). Prescription: declare it, or
697        /// fix the id.
698        pub const STATE_UNDECLARED: DwCode = DwCode::new("DW0500", ExitTier::Build);
699    }
700
701    crate::dw_code! {
702        /// An asset's licence is outside the ADR-0013 allowlist, or its record lacks
703        /// a field the allowlist's rule for that asset requires. One code for every
704        /// asset that records a licence: a prefab catalog card (`delvec prefab`,
705        /// `delvec::admit::diag::DW_LICENSE`) and an image a campaign declares in
706        /// `world.textures[]` (spec-0084 §6.3, `crate::license`).
707        pub const LICENSE_REFUSED: DwCode = DwCode::new("DW0741", ExitTier::Build);
708    }
709    crate::dw_code! {
710        /// (spec-0084 §6.2) A `world.textures[]` row's file is not an image the
711        /// named texture can be replaced by: not a PNG, not `k·w₀ × k·h₀` of the
712        /// vanilla frame (or `k·w₀ × n·k·h₀` with a sidecar), a sidecar that does not
713        /// parse as vanilla's animation metadata, or bytes identical to vanilla's.
714        pub const TEXTURE_IMAGE: DwCode = DwCode::new("DW0940", ExitTier::Build);
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    /// The tier's arithmetic is the CLI's published contract
723    /// (`docs/reference/compiler.md` §1), so it is asserted rather than left to
724    /// whoever next reads the `match`.
725    #[test]
726    fn a_tier_maps_to_its_published_exit_status() {
727        assert_eq!(ExitTier::Analysis.exit_status(), 2);
728        assert_eq!(ExitTier::Build.exit_status(), 3);
729    }
730
731    /// A code carries the tier it was declared with, independently of what its
732    /// number happens to spell — which is the
733    /// whole point of moving the tier off the code's spelling. `DW0312` is the
734    /// live instance: a `DW03xx` number that exits 2.
735    #[test]
736    fn a_code_carries_the_tier_it_declares_not_the_one_its_number_spells() {
737        // `let`, not `const`: a `const NAME: DwCode = …` here would be a SECOND
738        // diagnostic constant declaring a live code, and `tools/ci/check-dw-codes.py`
739        // reads every `crates/**/*.rs` — it refuses one code declared twice, and
740        // it is right to. Measured: this test written with `const` reds that gate
741        // on all three codes.
742        let analysis_spelt_dw03 = DwCode::new("DW0312", ExitTier::Analysis);
743        let build_spelt_dw03 = DwCode::new("DW0311", ExitTier::Build);
744
745        assert_eq!(analysis_spelt_dw03.exit_tier(), ExitTier::Analysis);
746        assert_eq!(analysis_spelt_dw03.exit_tier().exit_status(), 2);
747        assert_eq!(build_spelt_dw03.exit_tier().exit_status(), 3);
748    }
749
750    /// A code's properties are independent, and `about_the_engine`
751    /// rebuilds the struct field by field — the one place where setting one
752    /// could silently reset another. Today it cannot (there is no `Default` and
753    /// no struct-update syntax, so an omitted field is a compile error), but
754    /// "the compiler would catch it" is a claim about the current shape, and
755    /// this is the assertion that survives the shape changing.
756    #[test]
757    fn marking_a_code_an_engine_notice_keeps_its_tier() {
758        let engine_notice = DwCode::new("DW0813", ExitTier::Analysis).about_the_engine();
759        assert_eq!(engine_notice.subject(), Subject::Engine);
760        assert_eq!(engine_notice.exit_tier(), ExitTier::Analysis);
761        assert_eq!(engine_notice.id(), "DW0813");
762    }
763}