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)]
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/// The stable validation diagnostic codes (catalogued in
350/// `docs/reference/compiler.md` §5).
351///
352/// Every entry is a [`DwCode`], so every entry states its exit tier — there is
353/// no way to add one that does not.
354pub mod codes {
355 use super::{DwCode, ExitTier};
356
357 /// Document does not conform to its stage schema (unknown field / wrong type).
358 pub const SCHEMA: DwCode = DwCode::new("DW0100", ExitTier::Build);
359 /// Envelope `stage` does not match the document's slot.
360 pub const STAGE_MISMATCH: DwCode = DwCode::new("DW0101", ExitTier::Build);
361 /// Unsupported `dsl_version`.
362 pub const DSL_VERSION: DwCode = DwCode::new("DW0102", ExitTier::Build);
363 /// Inconsistent `campaign_id` across stages.
364 pub const CAMPAIGN_ID_MISMATCH: DwCode = DwCode::new("DW0103", ExitTier::Build);
365 /// Malformed id syntax (kebab-case / prefix).
366 pub const ID_SYNTAX: DwCode = DwCode::new("DW0110", ExitTier::Build);
367 /// Duplicate id within its namespace.
368 pub const ID_DUPLICATE: DwCode = DwCode::new("DW0111", ExitTier::Build);
369 /// Dangling reference: an id ref does not resolve.
370 pub const DANGLING_REF: DwCode = DwCode::new("DW0112", ExitTier::Build);
371 /// Stage-6 dialogue node unreachable from `root`.
372 pub const DIALOGUE_UNREACHABLE: DwCode = DwCode::new("DW0120", ExitTier::Build);
373 /// Stage-6 dialogue `root`/`next` references an unknown node.
374 pub const DIALOGUE_BAD_REF: DwCode = DwCode::new("DW0121", ExitTier::Build);
375 /// Stage-6 dialogue effect references an objective that is unknown, not a
376 /// `talk-to`, or a `talk-to` on a different NPC (foreign effect).
377 pub const DIALOGUE_BAD_OBJECTIVE: DwCode = DwCode::new("DW0122", ExitTier::Build);
378 /// A stage-5 `talk-to` objective has no reachable completing dialogue option
379 /// (the static half of the compiler's `DW0203` deadlock guarantee).
380 pub const DIALOGUE_UNCOVERED: DwCode = DwCode::new("DW0123", ExitTier::Build);
381 /// Quest dependency cycle.
382 pub const PLAN_CYCLE: DwCode = DwCode::new("DW0130", ExitTier::Build);
383 /// `finale` is not a declared quest.
384 pub const FINALE_UNKNOWN: DwCode = DwCode::new("DW0131", ExitTier::Build);
385 /// `finale` is not the convergent sink of the plan: some declared quest is
386 /// not a transitive dependency of it.
387 ///
388 /// **The name deliberately does not contain `FINALE_UNREACHABLE`, which
389 /// belongs to `DW0201`.** That code says the finale can never complete; this
390 /// one says nothing at all about the finale being reachable — in the fixture
391 /// that raises it the finale completes perfectly well and a side trip hangs
392 /// off the plan. Both are `DwCode`, so nothing but the name distinguishes
393 /// them at a call site, and `tools/check-dw-codes.py` credits a bare
394 /// constant name mentioned in a crate's tests to **that crate's** code — so
395 /// one shared name would buy coverage for whichever rule the file happens to
396 /// sit next to.
397 pub const PLAN_NOT_CONVERGENT: DwCode = DwCode::new("DW0132", ExitTier::Build);
398 /// An optional quest inside the finale's dependency closure (spec-0051
399 /// §8.1) — including a finale that declares itself optional.
400 pub const OPTIONAL_ON_SPINE: DwCode = DwCode::new("DW0866", ExitTier::Build);
401 /// A mandatory quest whose `depends_on` edge or stage-5 `quest-complete`
402 /// trigger names an optional quest (spec-0051 §8.2).
403 pub const MANDATORY_ON_OPTIONAL: DwCode = DwCode::new("DW0867", ExitTier::Build);
404 /// A mandatory objective gated on a flag only an optional quest produces
405 /// (spec-0051 §8.3) — the mainline key behind participation.
406 ///
407 /// The participation-minimal replay (`DW0204`) is the compensating stronger
408 /// check behind it; this one refuses at the edge so the message can name
409 /// the strand.
410 pub const MAINLINE_KEY_OPTIONAL: DwCode = DwCode::new("DW0868", ExitTier::Build);
411 /// Objective `after` cycle.
412 pub const AFTER_CYCLE: DwCode = DwCode::new("DW0140", ExitTier::Build);
413 /// Anchor not provided by the area's bound prefab.
414 pub const ANCHOR_UNRESOLVED: DwCode = DwCode::new("DW0142", ExitTier::Build);
415 /// Item id not in the pinned 1.21.11 registry.
416 pub const ITEM_UNKNOWN: DwCode = DwCode::new("DW0143", ExitTier::Build);
417 /// (spec-0021) An `equipment` or `loot` enchantment id is not in the pinned
418 /// 1.21.11 enchantment registry.
419 pub const ENCHANTMENT_UNKNOWN: DwCode = DwCode::new("DW0433", ExitTier::Build);
420 /// (spec-0021) An enchantment level is outside the 1..=255 range vanilla's
421 /// `minecraft:enchantments` component can carry.
422 pub const ENCHANTMENT_LEVEL: DwCode = DwCode::new("DW0434", ExitTier::Build);
423 /// (spec-0021) Two `loot` entries target the same anchor, so one would
424 /// silently overwrite the other's contents.
425 pub const LOOT_DUPLICATE_ANCHOR: DwCode = DwCode::new("DW0435", ExitTier::Build);
426 /// (spec-0021) A `loot` declaration carries more stacks than the container
427 /// it fills has slots.
428 pub const LOOT_TOO_MANY_ITEMS: DwCode = DwCode::new("DW0432", ExitTier::Build);
429 /// A single-slot fill's `count` exceeds the item's `minecraft:max_stack_size`
430 /// in the pinned 1.21.11 registry. `item replace … container.<n> with <item>
431 /// <count>` fails **silently** above the cap, shipping an empty slot.
432 pub const ITEM_COUNT_OVER_STACK: DwCode = DwCode::new("DW0436", ExitTier::Build);
433 /// An `interact` declares `missing_item_hint` without a `requires_item`: the
434 /// hint answers a gate that does not exist, so it could never narrate.
435 pub const MISSING_ITEM_HINT_WITHOUT_ITEM: DwCode = DwCode::new("DW0437", ExitTier::Build);
436 /// Planned quest (stage 4) has no expansion in stage 5.
437 pub const QUEST_NOT_EXPANDED: DwCode = DwCode::new("DW0150", ExitTier::Build);
438 /// Stage-5 quest is not planned in stage 4.
439 pub const QUEST_NOT_PLANNED: DwCode = DwCode::new("DW0151", ExitTier::Build);
440 /// Stage-2 NPC has no stage-6 dialogue tree.
441 pub const NPC_WITHOUT_TREE: DwCode = DwCode::new("DW0152", ExitTier::Build);
442 /// Stage-6 dialogue tree references an NPC not declared in stage 2.
443 pub const TREE_WITHOUT_NPC: DwCode = DwCode::new("DW0153", ExitTier::Build);
444 /// Area binds neither or both of `prefab` / `prefab_pool` (exactly one
445 /// required).
446 pub const PREFAB_BINDING: DwCode = DwCode::new("DW0160", ExitTier::Build);
447 /// Area `prefab_pool` references a pool absent from `prefabs/` metadata.
448 pub const POOL_UNKNOWN: DwCode = DwCode::new("DW0161", ExitTier::Build);
449 /// Area `prefab` names a piece absent from `prefabs/` metadata — the same
450 /// obligation [`POOL_UNKNOWN`] carries on the other arm of the binding. It
451 /// is an error rather than a deferral because an area whose piece is absent
452 /// contributes no anchor set at all, so every per-area anchor proof over it
453 /// is SKIPPED rather than failed: a misspelling here is strictly less
454 /// checked than a correct name.
455 pub const PREFAB_UNKNOWN: DwCode = DwCode::new("DW0856", ExitTier::Build);
456 /// (v0.6, spec-0017) A stage-7 edit script is structurally invalid: an edit
457 /// names a region no earlier `select` in its batch defined, a composition
458 /// (`union`/`intersect`/`subtract`) lists too few regions, a box `min`
459 /// exceeds `max` on an axis, a surface band's `from` exceeds `to`, a palette
460 /// recipe is empty / carries a non-positive or non-finite weight or `scale`,
461 /// a `matching` list is empty, or a morph `by`/`passes` is 0. (Unknown block
462 /// ids in recipes reuse [`BLOCK_UNKNOWN`] / `DW0193`; id-syntax and
463 /// duplicate-name violations reuse `DW0110`/`DW0111`.)
464 pub const EDIT_INVALID: DwCode = DwCode::new("DW0162", ExitTier::Build);
465 /// (v0.3) A `kill` objective or `spawn-wave` effect references a `wave/<id>`
466 /// not declared in the stage-5 `waves` section (dangling wave reference).
467 pub const WAVE_UNKNOWN: DwCode = DwCode::new("DW0170", ExitTier::Build);
468 /// (v0.3) A declared wave is referenced by a `kill` objective but is never
469 /// spawned by any `spawn-wave` effect (referenced-but-never-spawned). A wave
470 /// must be spawned by some effect before its kill objective is reachable.
471 pub const WAVE_NEVER_SPAWNED: DwCode = DwCode::new("DW0171", ExitTier::Build);
472 /// (v0.3) A `requires_flags` entry references a `flag/<id>` that no `set-flag`
473 /// effect ever produces (dangling flag reference).
474 pub const FLAG_UNKNOWN: DwCode = DwCode::new("DW0172", ExitTier::Build);
475 /// (spec-0016 §1) A wave declares `respawns_on_rest: true` but the campaign
476 /// declares no `bonfire` — nothing can ever re-seat it, so the field is a
477 /// silent no-op. Either add the bonfire the re-seat is meant to hang off, or
478 /// drop the field.
479 pub const REST_RESEAT_NO_BONFIRE: DwCode = DwCode::new("DW0370", ExitTier::Build);
480 /// (spec-0016 §1) The campaign places a `bonfire`
481 /// but no class kit declares a `flask`. Resting replenishes the flask to its
482 /// declared count; with no flask the rest interaction's whole recovery half
483 /// is a no-op and the souls loop has no consumable to spend, so this is a
484 /// build error rather than a design choice.
485 pub const BONFIRE_NO_FLASK: DwCode = DwCode::new("DW0476", ExitTier::Build);
486 /// **An item gate a class cannot bring.** An objective completes only for a
487 /// player holding a named item, and some class's player has no way to be
488 /// holding it: the item's only source in the whole campaign is *another*
489 /// class's kit, or it has no source at all.
490 ///
491 /// A delve is played by one to four players who each pick one class, so an
492 /// objective reachable only by one class's pick is an objective a party can
493 /// be assembled unable to finish — and the party finds out at the thing they
494 /// cannot press. Quantified over EVERY class for the same reason
495 /// [`BONFIRE_NO_FLASK`] is: one class that cannot bring it is as broken as
496 /// none, because a solo player of that class is a supported party.
497 pub const ITEM_GATE_UNBRINGABLE: DwCode = DwCode::new("DW0849", ExitTier::Build);
498 /// (spec-0016 §1) A kit item's potion `contents`
499 /// is not something 1.21.11 can pour: declared on an item that carries no
500 /// `minecraft:potion_contents` component, empty (neither a named potion nor
501 /// an effect), an unknown potion or status-effect id, an amplifier or
502 /// duration outside the field vanilla stores it in, a lasting effect with no
503 /// `duration`, an instantaneous one *with* a duration, or a malformed
504 /// `color`.
505 pub const KIT_POTION_INVALID: DwCode = DwCode::new("DW0486", ExitTier::Build);
506 /// (spec-0016 §1) A potion-bearing kit item
507 /// declares no `contents` at `dsl_version` 0.8.0 — the Uncraftable Potion, a
508 /// bottle that pours nothing. The placeholder flask, as a build error.
509 pub const KIT_POTION_MISSING: DwCode = DwCode::new("DW0487", ExitTier::Build);
510 /// A `drops[]` `slot` entry does not
511 /// name a distinct slot the same entity's `equipment` actually fills — the
512 /// slot is empty, or the same slot is declared twice. A mob can only leave
513 /// behind a piece it wears, and it can only leave it behind once.
514 pub const DROP_SLOT_UNFILLED: DwCode = DwCode::new("DW0490", ExitTier::Build);
515 /// `drops[]` on an encounter that is
516 /// not billed `elite` or `boss`. Only a named fight leaves anything behind;
517 /// an ordinary mob's kit is never farmable (no-grind constitution), so the
518 /// declaration is refused rather than silently making rank-and-file gear
519 /// lootable.
520 pub const DROP_NOT_TIERED: DwCode = DwCode::new("DW0491", ExitTier::Build);
521 /// A `collect` `dropped_by` is not backed by the wave it names:
522 /// the wave declares no `{item}` drop of this objective's item, the count
523 /// asks for more copies than the wave's mobs can yield, or the objective
524 /// also declares a `container` (the item cannot come out of a box *and* off
525 /// a body).
526 pub const DROP_COLLECT_UNSOURCED: DwCode = DwCode::new("DW0492", ExitTier::Build);
527 /// A `collect` `dropped_by` is not ordered after the fight that
528 /// produces it: no `kill` objective for that wave precedes this collect in
529 /// the objective graph. Without that edge "kill the boss, take its key" is
530 /// an authoring intention the quest graph cannot prove, and the collect
531 /// reads as reachable from the campaign's first tick.
532 pub const DROP_COLLECT_UNORDERED: DwCode = DwCode::new("DW0493", ExitTier::Build);
533 /// (spec-0031, DSL v0.10) A `lethal_volumes[]` entry's `message` is blank.
534 ///
535 /// The volume would still kill — and would kill in silence, which is the one
536 /// thing the declaration exists to prevent. There is no compiler default that
537 /// could be right for a cliff, a lava pit and an acid pool at once, so a blank
538 /// wording is refused rather than papered over: a gate that reports green
539 /// while the player learns nothing is exactly the vacuous pass CLAUDE.md names.
540 pub const LETHAL_MESSAGE_BLANK: DwCode = DwCode::new("DW0512", ExitTier::Build);
541 /// (spec-0031, DSL v0.10) **A grant whose removal is a later effect, not its
542 /// own duration.** A `give-effect` is still live at the moment a
543 /// `clear-effect` for the same effect fires in the same bundle, so the clear
544 /// — not the duration — is what ends it.
545 ///
546 /// A bundle that does not reach its end leaves the effect on the player: a
547 /// logout, a crash, a death mid-chain, a `sequence` whose remaining
548 /// `schedule` never runs. A duration expires with no cooperation from
549 /// anything, which is why `seconds` is mandatory and why vanilla's `infinite`
550 /// is absent from this surface — this diagnostic is what stops the same
551 /// hazard being rebuilt out of two effects that are individually fine.
552 pub const EFFECT_CLEARED_LIVE: DwCode = DwCode::new("DW0540", ExitTier::Build);
553 /// (spec-0031, DSL v0.10) A `give-effect`'s `seconds` is zero or beyond
554 /// [`crate::MAX_EFFECT_SECONDS`], or its `amplifier` is beyond
555 /// [`crate::MAX_POTION_AMPLIFIER`].
556 ///
557 /// Zero is the grant that never happens — the unbound-vacuity class as a
558 /// number. The ceilings are vanilla's own field widths, so a duration typed
559 /// in ticks or milliseconds is caught instead of silently overflowing.
560 pub const EFFECT_GRANT_BOUNDS: DwCode = DwCode::new("DW0541", ExitTier::Build);
561 /// (v0.3) A wave mob `entity` is not a known vanilla entity id. (Item-id
562 /// checks for `collect.item`, `interact.requires_item` and `give-item.item`
563 /// reuse [`ITEM_UNKNOWN`] / `DW0143`.)
564 pub const ENTITY_UNKNOWN: DwCode = DwCode::new("DW0173", ExitTier::Build);
565 /// (i18n) An l10n sidecar does not correctly cover a declared language: the
566 /// `l10n/<code>.json` file is absent, its envelope (`campaign_id` / `lang` /
567 /// `dsl_version`) is inconsistent, or it is **missing** a key from the
568 /// authoritative inventory (under-coverage). English (`en`) is implicit and
569 /// never declared, so it is never checked.
570 pub const L10N_MISSING: DwCode = DwCode::new("DW0180", ExitTier::Build);
571 /// (i18n) An l10n sidecar carries an **orphan** key that is not in the
572 /// authoritative string inventory derived from the stage docs (over-coverage).
573 pub const L10N_ORPHAN: DwCode = DwCode::new("DW0181", ExitTier::Build);
574 /// (i18n / harness oracle) A player-visible string — authored English or any
575 /// sidecar translation — contains the reserved completion-marker sigil
576 /// `[dw:complete`. That chat sequence is the validation bot's per-objective
577 /// completion oracle; content carrying it could forge a passing critical-path
578 /// step. The channel is reserved, not merely conventional.
579 pub const MARKER_RESERVED: DwCode = DwCode::new("DW0182", ExitTier::Build);
580 /// (i18n v2) A player-visible string — authored English or any sidecar
581 /// translation — contains a character from the reserved private-use block the
582 /// compiler uses to carry an l10n key from the stage docs to the text
583 /// component it is emitted into ([`crate::l10n::TR_SIGIL`]). Content carrying
584 /// it could impersonate a translation tag, or survive into the datapack and
585 /// render as a tofu box. The block is reserved, not merely conventional.
586 pub const TR_SIGIL_RESERVED: DwCode = DwCode::new("DW0183", ExitTier::Build);
587 /// (i18n v2) A declared language has no entry in the Minecraft language-code
588 /// mapping table ([`crate::l10n::mc_lang_code`]), so the resource pack has no
589 /// filename to write its `assets/delvewright/lang/<code>.json` under. A
590 /// language is never silently dropped: either the code is corrected to a
591 /// mapped one, or the table gains the entry.
592 pub const LANG_CODE_UNMAPPED: DwCode = DwCode::new("DW0184", ExitTier::Build);
593 /// (i18n v2) A campaign l10n sidecar defines a key in the reserved
594 /// `delvewright.` **chrome** namespace ([`crate::chrome`]). Those are the
595 /// engine's own on-screen strings — `New objective: `, `Choose your class`,
596 /// the default a bonfire shows — owned by the compiler, translated with it,
597 /// and authored by no campaign; a sidecar row under that prefix would be
598 /// written into the language file and silently replace product chrome for that
599 /// language. The namespace is reserved, not merely conventional.
600 pub const CHROME_RESERVED: DwCode = DwCode::new("DW0186", ExitTier::Build);
601 /// (i18n v2) An l10n sidecar row was translated from English the campaign no
602 /// longer holds: its `source` entry differs from the key's canonical English.
603 /// The translation is present, applied and **wrong**, and no key-set check can
604 /// see it — `DW0180`/`DW0181` compare key SETS, and a rewritten line moves no
605 /// key. Load-bearing for entity display names, whose key belongs to the first
606 /// site declaring a given text, so renaming one body can migrate a key to
607 /// another body and the row that goes stale is not the one the author edited.
608 pub const L10N_STALE: DwCode = DwCode::new("DW0187", ExitTier::Build);
609 /// (i18n v2) An l10n sidecar records provenance for only some of its rows (or
610 /// none), so `DW0187` cannot see the rest. A warning, not an error: the
611 /// `source` map is additive, and this is the one-version deprecation window
612 /// before it is required. It states the unguarded row count, so an
613 /// unadopted sidecar is a reported number on every run rather than silence
614 /// that reads like a pass.
615 pub const L10N_PROVENANCE_MISSING: DwCode = DwCode::new("DW0188", ExitTier::Build);
616 /// (v0.4) A mannequin NPC `skin.texture_id` is malformed (not a bare kebab
617 /// token) or duplicated across NPCs (spec-0009). A missing `model` is a
618 /// schema error (`DW0100`); a missing PNG is a build error (`DW0309`).
619 pub const SKIN_INVALID: DwCode = DwCode::new("DW0190", ExitTier::Build);
620 /// (v0.4) A `talk-to` objective has no **ungated** reachable completing
621 /// dialogue option — every completing option is `requires_flags`-gated, so
622 /// the objective can deadlock the moment it activates (spec-0008 §1). Keep at
623 /// least one ungated completing path.
624 pub const DIALOGUE_FLAG_DEADLOCK: DwCode = DwCode::new("DW0191", ExitTier::Build);
625 /// (v0.4) A wave mob `effects[].effect` is not a known 1.21.11 effect id.
626 pub const EFFECT_UNKNOWN: DwCode = DwCode::new("DW0192", ExitTier::Build);
627 /// (v0.4) A `set-block` / `interact.prop` block id is not a known 1.21.11
628 /// block id.
629 pub const BLOCK_UNKNOWN: DwCode = DwCode::new("DW0193", ExitTier::Build);
630 /// (v0.4) An environment trigger id is malformed (`DW0110`-style) or
631 /// duplicated within the stage-5 `triggers` namespace.
632 pub const TRIGGER_INVALID: DwCode = DwCode::new("DW0194", ExitTier::Build);
633 /// (v0.4) A dialogue `talk-to` or `interact` objective targets an NPC after a
634 /// `despawn-npc` removes it on a reachable path (spec-0008 §5).
635 pub const NPC_DESPAWNED_REF: DwCode = DwCode::new("DW0195", ExitTier::Build);
636 /// (v0.5) An area `lighting.min_light` is out of the 1..=14 range (spec-0010).
637 pub const LIGHTING_RANGE: DwCode = DwCode::new("DW0196", ExitTier::Build);
638 /// (v0.6) A stage-2 NPC declares `deferred: true` but **no** `spawn-npc` effect
639 /// anywhere in the campaign ever summons it — the NPC never enters the world,
640 /// so its dialogue tree and any `talk-to` on it are unreachable content. The
641 /// NPC-lifecycle dual of [`NPC_DESPAWNED_REF`] / `DW0195`.
642 ///
643 /// (0197/0198 were *reserved* by spec-0011's draft and released when that spec
644 /// renumbered to `DW0340`/`DW0341`; they were never emitted by any code.)
645 pub const NPC_NEVER_SPAWNED: DwCode = DwCode::new("DW0197", ExitTier::Build);
646 /// (v0.6) A `talk-to` on a `deferred` NPC activates before the NPC can exist:
647 /// every `spawn-npc` for it sits in a quest that is a strict *descendant* of the
648 /// objective's quest on the stage-4 DAG (and none fires from a trigger or
649 /// dialogue), so the objective provably activates on an empty anchor.
650 pub const NPC_SPAWNED_LATE: DwCode = DwCode::new("DW0198", ExitTier::Build);
651 /// (v0.6) A `cutscene` effect's shape is invalid: it mixes the multi-shot
652 /// `shots` list with the single-shot `path`/`seconds` fields, gives neither,
653 /// or declares a shot with an empty camera `path`. A cutscene must resolve to
654 /// at least one shot, and every shot to at least one camera position.
655 pub const CUTSCENE_SHAPE: DwCode = DwCode::new("DW0199", ExitTier::Build);
656
657 /// (v0.6) `horizon: "ocean"` declared without a `boundary` (spec-0013):
658 /// validation-tier (exit 1). An infinite swimmable sea with no return rule is
659 /// an authoring error. Grouped in the DW032x world/region family by domain;
660 /// unlike the compiler-tier DW030x geometry codes it is raised at DSL
661 /// validation, so it exits 1.
662 pub const OCEAN_NO_BOUNDARY: DwCode = DwCode::new("DW0320", ExitTier::Build);
663 /// (v0.6) `boundary.margin` outside the `0..=64` range (spec-0013):
664 /// validation-tier (exit 1).
665 pub const BOUNDARY_MARGIN: DwCode = DwCode::new("DW0321", ExitTier::Build);
666 /// A stage-1 `horizon` param is out of range, or is a param of a base other
667 /// than the one declared (spec-0026): validation-tier (exit 1).
668 pub const HORIZON_PARAM: DwCode = DwCode::new("DW0853", ExitTier::Build);
669 /// A `horizon` whose base BUILDS terrain, on a campaign that states no
670 /// extent for that terrain to stand around (spec-0026): validation-tier
671 /// (exit 1).
672 ///
673 /// A surround rings a declared extent — a site plan's `region`. A campaign
674 /// that seats its pieces with `areas[]` declares none, and the union of
675 /// whatever gets placed is not a substitute: it is an artifact of the
676 /// compiler's fixed area stride, mostly the void between areas, so ringing
677 /// it builds a mountain range around empty space.
678 pub const SURROUND_NO_REGION: DwCode = DwCode::new("DW0855", ExitTier::Build);
679 /// (v0.6) A `sequence` effect is nested inside another `sequence` (directly, or
680 /// reachable via a nested `move-actor` `on_arrive`) — timelines do not recurse
681 /// (spec-0014). Flatten the inner steps into the outer timeline.
682 pub const NESTED_SEQUENCE: DwCode = DwCode::new("DW0329", ExitTier::Build);
683
684 /// (v0.6) Trap declaration structurally invalid (spec-0011): a malformed or
685 /// duplicated `trap/<id>`, an `at`/`disarm.via` that no area's prefab provides,
686 /// or a trap whose `disarm.via` collides with its own trigger anchor.
687 /// Validation-tier (exit 1). Renumbered off the spec's stale reserved number
688 /// (0197 — since taken).
689 pub const TRAP_INVALID: DwCode = DwCode::new("DW0340", ExitTier::Build);
690 /// (spec-0016 §2) A `shortcut` declaration is structurally invalid: a
691 /// malformed or duplicate `shortcut/<id>`, a `gate`/`unlock` anchor no area's
692 /// prefab provides, or a `gate` that IS the `unlock` (the mechanism must sit
693 /// on the far side, not in the doorway).
694 pub const SHORTCUT_INVALID: DwCode = DwCode::new("DW0371", ExitTier::Build);
695 /// (spec-0016 §2) A `close-gate` effect targets a gate a `shortcut` owns.
696 /// A shortcut opens **permanently** — that is the whole pattern — so its
697 /// permanence is structural: there is no verb that can put it back. Use a
698 /// different gate for the point-of-no-return beat.
699 pub const SHORTCUT_RESEALED: DwCode = DwCode::new("DW0372", ExitTier::Build);
700 /// (spec-0016 §3) An `ambush` declaration is structurally invalid: a
701 /// malformed or duplicate `ambush/<id>`, an empty `actors` list (an ambush
702 /// that ambushes nobody), or the same actor listed twice (the second
703 /// `spawn-actor` is a guarded no-op, so the author's intent silently halves).
704 /// The telegraph is deliberately NOT required — an un-telegraphed ambush is
705 /// core souls vocabulary.
706 pub const AMBUSH_INVALID: DwCode = DwCode::new("DW0375", ExitTier::Build);
707 /// (spec-0016 §4) A `timed-gate` declaration is structurally invalid: a
708 /// malformed or duplicate `timed-gate/<id>`, an `open_ticks` or
709 /// `closed_ticks` of 0 (a gate that never opens, or never closes — neither is
710 /// a timing gate), a `phase` at or beyond the full cycle, or a gate another
711 /// `timed-gate` or a `shortcut` already owns (two clocks fighting over one
712 /// region, or a clock fighting a permanent open), or a `disarm.via` anchor no
713 /// area's prefab provides / one that IS the gate anchor (the jam lever cannot
714 /// live inside the span it stops).
715 pub const TIMED_GATE_INVALID: DwCode = DwCode::new("DW0377", ExitTier::Build);
716 /// A `close-gate` effect targets the gate of a `timed-gate` that
717 /// declares a `disarm`. A disarm suppresses the clock **permanently with the
718 /// gate resting open** — a jammed portcullis stays up — so, exactly like a
719 /// `shortcut` (`DW0372`), its permanence is structural: there is no verb that
720 /// can re-arm it. Use a different gate for the beat that must re-seal, or drop
721 /// the `disarm`.
722 pub const TIMED_GATE_REARMED: DwCode = DwCode::new("DW0389", ExitTier::Build);
723 /// (spec-0016 §6) A wave's TD `lane` / `summon` declaration is structurally
724 /// invalid or internally contradictory: an empty `waypoints` list, a
725 /// waypoint anchor no area's prefab provides, a repeated consecutive
726 /// waypoint, an `aggro_radius` outside `4..=64`, a mob whose
727 /// `attributes.follow_range` disagrees with `aggro_radius` (they MUST be
728 /// equal — a patrolling raider holds ground against a target it cannot
729 /// engage), or `lane` together with `summon: aggro-edge` (a lane IS the
730 /// routing; aggro-edge is its opposite).
731 pub const LANE_INVALID: DwCode = DwCode::new("DW0381", ExitTier::Build);
732 /// (spec-0016 §6) A lane wave contains a non-raider species. `Patrolling` /
733 /// `patrol_target` are Raider NBT: on anything else they are dropped and the
734 /// mob simply stands where it spawned. The admitted set is vanilla's own
735 /// `#minecraft:raiders` tag, read from the vendored tag table — never a
736 /// species list this engine keeps. Non-raiders use `summon: aggro-edge`
737 /// instead.
738 pub const LANE_NOT_RAIDER: DwCode = DwCode::new("DW0382", ExitTier::Build);
739 /// (spec-0016 §6) A lane wave fields fewer than 2 mobs. A lone patroller
740 /// sets `Patrolling:0b` on itself when it finds no companion within its
741 /// follow range (vanilla), so a one-mob lane cancels itself.
742 pub const LANE_SQUAD_TOO_SMALL: DwCode = DwCode::new("DW0383", ExitTier::Build);
743 /// (spec-0016 §6) A lane `pillager` is not holding a crossbow. Its only
744 /// attack goal is the crossbow goal, so a pillager that acquires a target it
745 /// has no runnable attack for freezes in place indefinitely — patrol blocked
746 /// by the target, nothing to run instead (live-verified deadlock).
747 pub const LANE_UNARMED: DwCode = DwCode::new("DW0384", ExitTier::Build);
748 /// (spec-0016 §6) A `summon: aggro-edge` wave mob declares no
749 /// `attributes.follow_range`. That radius IS the summon ring — the distance
750 /// at which the mob perceives the party — so it is authored, never guessed
751 /// from a vanilla defaults table the compiler cannot verify.
752 pub const AGGRO_EDGE_NO_RANGE: DwCode = DwCode::new("DW0385", ExitTier::Build);
753 /// (v0.6) A trap dispense-payload item id is not in the pinned 1.21.11 registry
754 /// (spec-0011; mirrors `DW0143`). Validation-tier (exit 1). Renumbered off the
755 /// spec's stale reserved number (0198 — since taken).
756 pub const TRAP_PAYLOAD_UNKNOWN: DwCode = DwCode::new("DW0341", ExitTier::Build);
757
758 /// (spec-0022) A trap declares **no consequence at all**: neither the legacy
759 /// redstone `effect` nor a command `payload`. A trap that does nothing is
760 /// mute hardware the completability proofs would nonetheless reason about,
761 /// so it is a content mistake, not a no-op. Validation-tier (exit 1).
762 pub const TRAP_NO_CONSEQUENCE: DwCode = DwCode::new("DW0440", ExitTier::Build);
763 /// (spec-0022) A `volley` `projectile` / `collapse` `falling_block` /
764 /// `then_floor` id is not in the pinned 1.21.11 registry (a `projectile`
765 /// must be an ENTITY id, the collapse blocks BLOCK ids).
766 /// Validation-tier (exit 1).
767 pub const TRAP_VERB_ID_UNKNOWN: DwCode = DwCode::new("DW0441", ExitTier::Build);
768 /// (spec-0022) A `volley`'s `salvos` / `interval` is out of range (`salvos`
769 /// in `1..=16`, `interval` in `1..=200`). A volley fires its whole kill zone
770 /// every salvo, so the entity count is `salvos x cells`; and salvos spread
771 /// wider than the interval cap stop reading as one trap event.
772 /// Validation-tier (exit 1).
773 pub const VOLLEY_CADENCE: DwCode = DwCode::new("DW0443", ExitTier::Build);
774
775 /// (v0.6) A `shot_style` declaration is semantically invalid (spec-0015 shot
776 /// grammar): a styled shot with no `subject`; style-only fields (`subject`,
777 /// `subject_b`, `dist`, `degrees`, `bearing`) on an unstyled shot; a
778 /// `subject_b` on a style other than `two-shot` (or a `two-shot` without
779 /// one); `degrees` off `orbit-arc` or outside `45..=120`; `dist` outside
780 /// `1..=48`; or `bearing` outside `-360..=360`. Validation-tier (exit 1).
781 pub const SHOT_STYLE_INVALID: DwCode = DwCode::new("DW0348", ExitTier::Build);
782 /// (v0.6) A `side-track` / `low-follow` shot whose subject has no
783 /// compiler-known motion: those styles dolly *with* a moving subject, so the
784 /// subject must be an NPC/actor with a matching `move-npc`/`move-actor` in
785 /// the same effect group or the same `sequence` timeline (an `anchor`
786 /// subject can never move). Validation-tier (exit 1). Use `locked-off` /
787 /// `push-in` for a static subject instead.
788 pub const SHOT_SUBJECT_UNMOVED: DwCode = DwCode::new("DW0349", ExitTier::Build);
789
790 /// (v0.4, added round-6) A `use` trigger anchored where an NPC stands.
791 /// Right-click on an NPC already belongs to its dialogue advancement; a
792 /// second interaction hitbox in the same cell makes the client's entity
793 /// ray-pick ambiguous, and whichever entity loses the tie is silently dead
794 /// — the round-6 island soft-lock class (an exactly co-located hitbox
795 /// starved the giant's dialogue of every right-click). `strike` triggers
796 /// are exempt: a left-click has no dialogue meaning, so the compiler rides
797 /// the trigger's tag on the NPC's own hitbox instead of summoning a second
798 /// one. Validation-tier (exit 1).
799 pub const USE_TRIGGER_ON_NPC: DwCode = DwCode::new("DW0350", ExitTier::Build);
800
801 /// (v0.6, spec-0018) `world.min_players` outside the `1..=4` range. A delve is
802 /// played by ONE party of 1–4 (ADR/CLAUDE.md product definition), so a declared
803 /// mandatory party size can never sit outside it. Validation-tier (exit 1).
804 pub const PARTY_SIZE: DwCode = DwCode::new("DW0356", ExitTier::Build);
805 /// (v0.6, spec-0018) A `carrier: "one"` `give-item` sits in a bundle that is
806 /// only ever reached from the **scheduler** (`move-npc`/`move-actor`
807 /// `on_arrive`, a `sequence` step). `carrier: "one"` means "hand this single
808 /// quest prop to the player whose action earned it"; a scheduled bundle runs
809 /// with the server command source and has no acting player, so there is no
810 /// defensible recipient. Give it to the whole party (drop `carrier`), or move
811 /// the hand-off onto the beat that a player completes. Validation-tier (exit 1).
812 pub const PARTY_CARRIER_SCHEDULED: DwCode = DwCode::new("DW0357", ExitTier::Build);
813
814 /// (v0.6) `world.difficulty` is `peaceful`. On
815 /// peaceful the server discards every hostile-category mob as it is ticked —
816 /// `/summon`ed, `NoAI`, `PersistenceRequired`, all of it — so a peaceful delve
817 /// is one in which every wave, every hostile actor and every ambush silently
818 /// ceases to exist. There is no delve that wants that, so the keyword is
819 /// refused rather than honoured. Validation-tier (exit 1).
820 pub const DIFFICULTY_INVALID: DwCode = DwCode::new("DW0468", ExitTier::Build);
821 /// (v0.6) A campaign fields scripted `actors[]` (an
822 /// ambush desugars into these too) but **no** `waves[]` and no declared
823 /// `world.difficulty`, so the compiler's historical derivation ships
824 /// `difficulty=peaceful` — under which every one of those actors that is a
825 /// hostile species is discarded on the tick it spawns. The compiler cannot
826 /// decide the question for the author: the pinned entity registry is a
827 /// membership set with no mob-category data, so "is this actor a monster" is
828 /// not something it can verify rather than guess. Advisory (warning,
829 /// exit 0) — declaring `world.difficulty` settles it either way.
830 pub const DIFFICULTY_UNDECLARED_ACTORS: DwCode = DwCode::new("DW0469", ExitTier::Build);
831 /// (spec-0016 §1, spec-0023, souls ruling 5/7: "stage bosses never respawn
832 /// on rest") A wave declares BOTH `tier: boss` and `respawns_on_rest: true`.
833 /// `tier` and `respawns_on_rest` are two fields on the same [`Wave`]
834 /// declaration — the only place a "boss" billing and a "re-seat on rest"
835 /// contract can land on one another; an [`Actor`] carries `tier` too but has
836 /// no `respawns_on_rest` field at all (it is killed by hand, never re-seated
837 /// by a bonfire), so this is the sole structurally expressible violation of
838 /// the ruling. A rest-respawning boss re-fight breaks the retry economy the
839 /// ruling exists to protect: a boss is the campaign's named fight, not
840 /// trash pressure the party grinds back down every rest. Validation-tier
841 /// (exit 1), `dsl::validate`. Prescription: drop `tier: boss` if the
842 /// encounter really is meant to re-seat (bill it `elite` instead), or drop
843 /// `respawns_on_rest` if it really is the boss.
844 ///
845 /// [`Wave`]: crate::stages::Wave
846 /// [`Actor`]: crate::stages::Actor
847 pub const BOSS_RESPAWNS_ON_REST: DwCode = DwCode::new("DW0499", ExitTier::Build);
848
849 // -- DSL v0.10 runtime state (spec-0031) ---------------------------------
850
851 /// (v0.10, spec-0031) A `state/<kebab>` reference — in a `requires_state`
852 /// comparison or in a `set-state`/`add-state`/`clear-state` verb — names a
853 /// datum the campaign never declares in the stage-5 `state` list. Unlike a
854 /// flag, a datum IS declared: its scope and its initial value are facts no
855 /// use site can supply, so an undeclared reference is not "a datum that
856 /// happens to start at zero", it is a datum with no defined multiplayer
857 /// semantics at all. Validation-tier (exit 1). Prescription: declare it, or
858 /// fix the id.
859 pub const STATE_UNDECLARED: DwCode = DwCode::new("DW0500", ExitTier::Build);
860 /// (v0.10, spec-0031) A gate's `requires_state` reads a declared datum that
861 /// **no verb anywhere in the campaign ever writes**. The datum can only ever
862 /// hold its declared `initial`, so the comparison's answer was decided at
863 /// authoring time and the gate is a constant wearing a condition's clothes.
864 ///
865 /// This is the vacuity rule at the level of one datum (CLAUDE.md: *a green
866 /// gate that binds to nothing is vacuous, not a pass*) — the numeric
867 /// equivalent of the bot's combat floor examining zero enemies for nineteen
868 /// rounds. Validation-tier (exit 1). Prescription: write the datum somewhere
869 /// (`set-state`/`add-state`/`clear-state`), or drop the comparison and say
870 /// what you meant unconditionally.
871 pub const STATE_NEVER_WRITTEN: DwCode = DwCode::new("DW0501", ExitTier::Build);
872 /// (v0.10, spec-0031) A declared datum that **no gate anywhere in the
873 /// campaign ever reads**. Either some verb writes it and nothing ever asks
874 /// (the write is inert — a counter nobody consults), or nothing touches it at
875 /// all (a dead declaration). Runtime state exists to be compared against; a
876 /// datum with no reader is bookkeeping no player can ever observe.
877 /// Validation-tier (exit 1). Prescription: gate something on it with
878 /// `requires_state`, or delete the declaration and its writes.
879 pub const STATE_NEVER_READ: DwCode = DwCode::new("DW0502", ExitTier::Build);
880 /// (v0.10, spec-0031) A `player`-scoped datum is referenced where emission
881 /// has no acting player to read or write it against.
882 ///
883 /// Two such places exist, and both are properties of the SITE, not of the
884 /// verb: a scheduler-only bundle (a `sequence` step, a `move-npc` /
885 /// `move-actor` `on_arrive`) runs with the server command source — the same
886 /// seam `DW0357` polices for `carrier: "one"` — and the gates emission
887 /// evaluates against the party holder rather than against a player (an
888 /// objective's activation guard, a trigger's arming gate, a trap's arming
889 /// gate) have no `@s` either. Validation-tier (exit 1). Prescription: declare
890 /// the datum `party`-scoped if the whole party shares it, or move the
891 /// read/write onto a site a player drives (a dialogue option, a cast
892 /// placement, an effect on a beat a player completes).
893 pub const STATE_SCOPE_UNREACHABLE: DwCode = DwCode::new("DW0503", ExitTier::Build);
894 /// (v0.10, spec-0032) A `stakes[]` declaration is unusable as a personal
895 /// wager: its `state` is a datum the campaign never declares, or one declared
896 /// `party`-scoped.
897 ///
898 /// **The scope half is the multiplayer decision most likely to be made by
899 /// accident** (spec-0032, stated for correction rather than left to emerge).
900 /// A stake is one player's loss and one player's chance to get it back; a
901 /// party-shared purse would turn a teammate's death into a penalty on
902 /// everyone, and nothing in the JSON would say so. Validation-tier (exit 1).
903 /// Prescription: declare the datum `player`-scoped, or point the stake at a
904 /// datum that is.
905 pub const STAKE_STATE_SCOPE: DwCode = DwCode::new("DW0520", ExitTier::Build);
906 /// (v0.10, spec-0032) A `drop-stake` effect names a stake the campaign never
907 /// declares in the stage-5 `stakes` list. Validation-tier (exit 1).
908 /// Prescription: declare it, or fix the id.
909 pub const STAKE_UNDECLARED: DwCode = DwCode::new("DW0521", ExitTier::Build);
910 /// (v0.10, spec-0032) A declared stake that **no `drop-stake` effect anywhere
911 /// in the campaign ever leaves**. The retention policy, the forfeit rule and
912 /// the whole placement table are computed for a mechanism no beat can fire —
913 /// a declaration wearing a feature's clothes.
914 ///
915 /// The same vacuity rule `DW0502` states for a datum with no reader
916 /// (CLAUDE.md: *a green gate that binds to nothing is vacuous, not a pass*).
917 /// Validation-tier (exit 1). Prescription: drop it from a beat — `on_death`
918 /// is the usual one — or delete the declaration.
919 pub const STAKE_NEVER_DROPPED: DwCode = DwCode::new("DW0522", ExitTier::Build);
920 /// (v0.10, spec-0032) A `shops[].offers[]` entry that cannot deliver
921 /// anything: it declares no `effects`, so its button is drawn, is pressable,
922 /// and does nothing.
923 ///
924 /// The shop analogue of the invisible-affordance rule: a control the player
925 /// can operate must have an observable answer. A refusal counts — an offer
926 /// whose only effect is a gated `narrate` saying "you cannot afford that" is
927 /// exactly the authored shape spec-0032 asks for. Validation-tier (exit 1).
928 /// Prescription: give the offer effects, or delete it.
929 pub const SHOP_OFFER_INERT: DwCode = DwCode::new("DW0523", ExitTier::Build);
930 /// (v0.10, spec-0032) A `forfeit` of kind `proportion` whose `percent` is
931 /// above 100 — a death that takes more than the whole purse. Validation-tier
932 /// (exit 1). Prescription: 0–100, or use `all`.
933 pub const STAKE_FORFEIT_RANGE: DwCode = DwCode::new("DW0524", ExitTier::Build);
934 /// (v0.10, spec-0032) **A comparison read after the bundle has already changed
935 /// what it compares.** An effect's `requires_state` names a datum that an
936 /// EARLIER effect in the same bundle writes, so the gate is evaluated against
937 /// the post-write value, not the value the beat started with.
938 ///
939 /// Found in the emitted output of spec-0032's own first shop. The authored
940 /// shape a shop wants is "the purchase behind `at-least 1`, the apology behind
941 /// `at-most 0`" — and written in that order, buying your LAST ember prints both:
942 /// the debit runs, the balance falls to 0, and the apology's gate — evaluated
943 /// after it — now holds. Vanilla evaluates each `execute` when it reaches it,
944 /// which is the whole reason a per-effect gate is useful, so this is not a bug
945 /// to fix in emission: it is an ordering hazard that only reading the generated
946 /// function reveals. The fix is always the same and always local — **put the
947 /// reading effect before the writing one** — which is why this is a warning
948 /// naming the earlier write rather than a refusal.
949 ///
950 /// Warning-tier (exit 0). Prescription: move the gated effect ahead of the
951 /// write, or gate it on something the bundle does not itself change.
952 pub const STATE_READ_AFTER_WRITE: DwCode = DwCode::new("DW0527", ExitTier::Build);
953
954 /// (v0.11) **A press answer addressed to a click vanilla cannot attribute.**
955 /// A trigger declares `audience: presser` on something other than an
956 /// `on: use`.
957 ///
958 /// `minecraft:player_interacted_with_entity` is the only vanilla criterion
959 /// that runs a function as the player who clicked, and it fires on
960 /// right-clicks alone. A left-click is recorded in the interaction entity's
961 /// `attack` NBT — a UUID no command can become — and an `approach` involves no
962 /// click at all. Approximating it (polling the record and assuming the nearest
963 /// player) is the downstream folklore CLAUDE.md's no-hack rule excludes, so the
964 /// capability is refused rather than faked.
965 pub const TRIGGER_AUDIENCE_UNATTRIBUTABLE: DwCode = DwCode::new("DW0427", ExitTier::Build);
966
967 /// (v0.11) **A trigger id in the compiler's reserved `dw-` namespace.** The
968 /// compiler synthesizes triggers of its own — today the press answer every
969 /// sealed gate and shortcut door gives (`trigger/dw-press-…`) — and two
970 /// triggers sharing an id would share one `dw_trig_…` tag and one emitted
971 /// function, so one of them would silently disappear. Reserving the prefix
972 /// makes the collision impossible by construction instead of improbable.
973 pub const TRIGGER_ID_RESERVED: DwCode = DwCode::new("DW0428", ExitTier::Build);
974
975 /// (v0.11) **A sealed body with no press answer**, uniformly over the
976 /// pressable class. A `shortcuts[]` door or
977 /// a `close-gate`'s wall is sealed, and nothing says what it answers when the
978 /// party presses it — no `use` trigger anchored on it, and (for a
979 /// `close-gate`) no authored `sealed_hint`.
980 ///
981 /// The compiler deliberately does **not** fill that silence. A baked default
982 /// is the compiler making a design statement — about tone, about what this
983 /// specific door is — on the author's behalf, and then never telling them it
984 /// did; an error makes the author say it. Same rule as "no hacks at any
985 /// layer": if content needs a thing, the DSL exposes it and the author
986 /// declares it, rather than a lower layer inventing it.
987 ///
988 /// One rule for the whole pressable class: two objects of the same class do
989 /// not get two defaulting policies, which would be the "capability keyed to
990 /// the verb" defect this very surface is CLAUDE.md's worked example of.
991 pub const SEALED_BODY_UNANSWERED: DwCode = DwCode::new("DW0429", ExitTier::Build);
992
993 /// (v0.11, spec-0034) **A declared locomotion the engine cannot hold the
994 /// body to** — today exactly one value, `aquatic`.
995 ///
996 /// The declaration surface exists so an author can claim a capability and
997 /// have the claim PROVEN. `aquatic` is the one
998 /// class that carries no exemption and governs no rule: it is a ledger
999 /// label the compiler derives from vanilla's own `#minecraft:aquatic` tag.
1000 /// Declaring it could therefore never change a verdict, so it would always
1001 /// land in `DW0454` — and a value whose only possible outcome is another
1002 /// diagnostic is a trap, not a surface.
1003 ///
1004 /// The gap it names, stated rather than left to folklore (CLAUDE.md's
1005 /// no-hack rule): the compiler routes **every** body on standable ground,
1006 /// and `flooded` cells are impassable and never floor for every body. There
1007 /// is no water-traversal model for a declaration to feed, so there is
1008 /// nothing to hold an aquatic claim to. When routing grows one, this
1009 /// refusal is what has to be deleted to enable the value.
1010 ///
1011 /// Error tier, raised in `validate_campaign_with`, so the run ends at the
1012 /// validation tier (exit 1). Prescription: remove the declaration — a body whose
1013 /// route crosses water is governed by the flooded-cell rules already, and
1014 /// the derived aquatic class still reaches the binding ledger.
1015 pub const TRAVERSAL_UNPROVABLE: DwCode = DwCode::new("DW0455", ExitTier::Build);
1016
1017 /// A gate contradicts itself, so it can NEVER open: a flag on both its
1018 /// `requires_flags` and `forbids_flags`, or `requires_state` terms on one
1019 /// datum that no integer satisfies (`at-least 5` with `at-most 3`, two
1020 /// different `equals`). The thing carrying it — objective, effect, trigger,
1021 /// trap, dialogue option, cast placement, shop offer — is authored content
1022 /// that provably never happens, which is a defect in what the document
1023 /// SAYS, not a stylistic lint. One rule over the whole closed consumer set
1024 /// ([`crate::gate::for_each_gate`]), because satisfiability is a property
1025 /// of the gate, never of the verb that first needed the question answered.
1026 ///
1027 /// Error tier, validation (exit 1).
1028 pub const GATE_NEVER_OPENS: DwCode = DwCode::new("DW0847", ExitTier::Build);
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 /// The tier's arithmetic is the CLI's published contract
1036 /// (`docs/reference/compiler.md` §1), so it is asserted rather than left to
1037 /// whoever next reads the `match`.
1038 #[test]
1039 fn a_tier_maps_to_its_published_exit_status() {
1040 assert_eq!(ExitTier::Analysis.exit_status(), 2);
1041 assert_eq!(ExitTier::Build.exit_status(), 3);
1042 }
1043
1044 /// A code carries the tier it was declared with, independently of what its
1045 /// number happens to spell — which is the
1046 /// whole point of moving the tier off the code's spelling. `DW0312` is the
1047 /// live instance: a `DW03xx` number that exits 2.
1048 #[test]
1049 fn a_code_carries_the_tier_it_declares_not_the_one_its_number_spells() {
1050 // `let`, not `const`: a `const NAME: DwCode = …` here would be a SECOND
1051 // diagnostic constant declaring a live code, and `tools/check-dw-codes.py`
1052 // reads every `crates/**/*.rs` — it refuses one code declared twice, and
1053 // it is right to. Measured: this test written with `const` reds that gate
1054 // on all three codes.
1055 let analysis_spelt_dw03 = DwCode::new("DW0312", ExitTier::Analysis);
1056 let build_spelt_dw03 = DwCode::new("DW0311", ExitTier::Build);
1057
1058 assert_eq!(analysis_spelt_dw03.exit_tier(), ExitTier::Analysis);
1059 assert_eq!(analysis_spelt_dw03.exit_tier().exit_status(), 2);
1060 assert_eq!(build_spelt_dw03.exit_tier().exit_status(), 3);
1061 }
1062
1063 /// A code's properties are independent, and `about_the_engine`
1064 /// rebuilds the struct field by field — the one place where setting one
1065 /// could silently reset another. Today it cannot (there is no `Default` and
1066 /// no struct-update syntax, so an omitted field is a compile error), but
1067 /// "the compiler would catch it" is a claim about the current shape, and
1068 /// this is the assertion that survives the shape changing.
1069 #[test]
1070 fn marking_a_code_an_engine_notice_keeps_its_tier() {
1071 let engine_notice = DwCode::new("DW0813", ExitTier::Analysis).about_the_engine();
1072 assert_eq!(engine_notice.subject(), Subject::Engine);
1073 assert_eq!(engine_notice.exit_tier(), ExitTier::Analysis);
1074 assert_eq!(engine_notice.id(), "DW0813");
1075 }
1076}