Skip to main content

delvewright_dsl/
placement.rs

1//! **Where a campaign's places and anchor names come from — asked once.**
2//!
3//! A campaign has exactly one placement authority. The usual one is stage-1
4//! `areas[]`, which seats prefab pieces on the compiler's fixed stride; a
5//! campaign planned as a whole map hands the space to its `site-plan.json`
6//! instead and the blockout is derived. `DW0839` refuses a campaign holding
7//! both.
8//!
9//! Every rule that resolves an area id or an anchor name already asks which of
10//! the two it is — [`crate::validate::AnchorProviders`] does it to build the
11//! resolvable set, and three area sets in `crate::validate` do it to admit
12//! [`crate::siteplan::SITE_AREA`]. What none of them asked is the second half of
13//! the same question: **when the reference does not resolve, what is this author
14//! allowed to write instead?**
15//!
16//! That half was hand-written beside each refusal at twenty sites, in the
17//! vocabulary of the only kind of campaign that existed when the rules were
18//! written. So a site-plan campaign was refused by `DW0112` with *"declare it in
19//! stage-1 `world.areas`"* — which is precisely what `DW0839` refuses in a
20//! campaign carrying a site plan, and which `DW0160` refuses again for having no
21//! prefab to bind — and by `DW0142` with *"anchor names come from prefab
22//! metadata; do NOT invent one"* against names that are synthesized by design and
23//! have no metadata anywhere.
24//!
25//! CLAUDE.md names the shape: **when one gate's prescription is another gate's
26//! refusal, the defect belongs to the PAIR**, and a gate that names a remedy owes
27//! a check that the remedy is reachable. The repair is the constitution's own —
28//! a capability belongs to the object class it acts on, so *"what may this author
29//! write"* is a property of the campaign's placement authority and is answered in
30//! one place, not re-decided at each verb.
31//!
32//! Nothing here decides whether a reference is wrong. Every predicate is exactly
33//! what it was; only the sentence the author reads afterwards is chosen by the
34//! campaign rather than by the site.
35
36use crate::envelope::Campaign;
37
38/// The one thing a campaign hands its space to.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Placement {
41    /// Stage-1 `areas[]` seats prefab pieces. Anchor names come from prefab
42    /// metadata; area ids are the ones the world document declares.
43    Prefabs,
44    /// A `site-plan.json` owns the space. There is exactly one area
45    /// ([`crate::siteplan::SITE_AREA`]) and the anchor names are the ones the
46    /// derivation places ([`crate::siteplan::synthesized_anchors`]) — the
47    /// `spawn`/`node-`/`seam-`/`unlock-` set the graph's own shape dictates,
48    /// plus every `stations[]` name its nodes declare (spec-0052). The station
49    /// half is why the remedy tells the author they may *declare* the name as
50    /// well as correct it: it is the only anchor name in this engine an author
51    /// writes by hand, so an unresolved one is as likely a missing declaration
52    /// as a typo.
53    SitePlan,
54    /// **Neither.** `areas[]` is empty and there is no site plan, so the campaign
55    /// declares no place and nothing places an anchor: every area reference and
56    /// every anchor reference in it is unresolvable, whatever it says.
57    ///
58    /// This is a real authoring state and it is the one the old messages served
59    /// worst — a story layer written before its map. Refusing it is correct; the
60    /// only question is what the author is told to do about it, and the answer
61    /// is not "declare it in `world.areas`", because that names one of the two
62    /// halves of a choice the author has not made yet and hides the other.
63    NoMap,
64}
65
66impl Placement {
67    /// Ask the campaign, once.
68    ///
69    /// A site plan wins when both are present, because that is what the
70    /// resolution does: [`crate::validate::AnchorProviders`] and all three area
71    /// sets admit the derived vocabulary whenever a plan is on disk, whatever
72    /// `areas[]` says. Such a campaign is refused by `DW0839`, whose message is
73    /// the one that matters there — this one is describing the set the resolver
74    /// actually used.
75    #[must_use]
76    pub fn of(c: &Campaign) -> Self {
77        if c.site_plan.is_some() {
78            Self::SitePlan
79        } else if c.world.content.areas.is_empty() {
80            Self::NoMap
81        } else {
82            Self::Prefabs
83        }
84    }
85
86    /// **What to write instead of an area id that does not resolve.**
87    ///
88    /// The `Prefabs` arm is the sentence every area refusal has always carried,
89    /// unchanged, so a prefab campaign reads exactly what it read before.
90    #[must_use]
91    pub fn area_remedy(self) -> &'static str {
92        match self {
93            Self::Prefabs => "declare it in stage-1 `world.areas` or correct the reference",
94            Self::SitePlan => {
95                "this campaign's map is its site plan, so it has exactly one area, \
96                 `area/site`. Point the reference at that, and do NOT declare the id in \
97                 `world.areas`: a campaign carrying a site plan declares an empty `areas` list \
98                 (`DW0839`), and an entry with no prefab bound to it is refused again \
99                 (`DW0160`)"
100            }
101            Self::NoMap => {
102                "this campaign declares no area at all: `world.areas` is empty and there is \
103                 no `site-plan.json`, so it has no map for a reference to land in. Give it one \
104                 placement authority, and only one (`DW0839`): either declare the area in \
105                 stage-1 `world.areas` with a `prefab` or `prefab_pool` bound to it, or write \
106                 the map pipeline (`geometry-brief.json`, then `layout-graph.json`, then \
107                 `site-plan.json`) and name the campaign's one area `area/site`"
108            }
109        }
110    }
111
112    /// **What to write instead of an anchor name that does not resolve.**
113    ///
114    /// `prefab` is the sentence this call site has always printed for a prefab
115    /// campaign, passed in rather than centralised because it is genuinely
116    /// per-verb — a trap is told about `anchor/trap` markers, a trigger about
117    /// its `at`. It is returned verbatim, so a prefab campaign's refusal is
118    /// byte-identical to the one it printed before this module existed.
119    ///
120    /// It is *dropped* on the other two arms rather than appended, and that is
121    /// the whole point: every one of those sentences prescribes a prefab
122    /// operation, and a derived map has no prefab for the author to reach.
123    #[must_use]
124    pub fn anchor_remedy(self, prefab: &str) -> &str {
125        match self {
126            Self::Prefabs => prefab,
127            Self::SitePlan => {
128                "this campaign's map is its site plan, so there is no prefab metadata to \
129                 read. Its anchor names are the ones the derivation places: `anchor/node-<place>` \
130                 for each place the layout graph declares, `anchor/seam-<edge>` over each barred \
131                 connection, `anchor/unlock-<edge>` on the far side of a one-sided one, and \
132                 `spawn` for the entry — plus every `stations[]` name its nodes declare. Write \
133                 one of those, or declare this name as a station on the node it belongs to, and \
134                 do NOT add an `areas[]` entry to get a prefab: a campaign carrying a site plan \
135                 declares an empty `areas` list (`DW0839`)"
136            }
137            Self::NoMap => {
138                "this campaign has no map yet: `world.areas` is empty and there is no \
139                 `site-plan.json`, so nothing places an anchor and no name can resolve. Give it \
140                 one placement authority, and only one (`DW0839`): either declare an area in \
141                 stage-1 `world.areas` with a `prefab` bound to it and write an anchor that \
142                 prefab's metadata exposes, or write the map pipeline (`geometry-brief.json`, \
143                 then `layout-graph.json`, then `site-plan.json`) and write one of the names its \
144                 derivation places (`anchor/node-<place>`, `anchor/seam-<edge>`, \
145                 `anchor/unlock-<edge>`, `spawn`, or a `stations[]` name a node declares)"
146            }
147        }
148    }
149
150    /// **Where this campaign writes a lighting declaration.**
151    ///
152    /// Not a sentence but a field name, because that is the whole of what moves
153    /// between the two kinds: a site plan carries ONE `lighting`, applied to
154    /// every enclosed box, and a prefab campaign carries one per `areas[]`
155    /// entry. The prose around it — which fixture, what `min_light` is for, what
156    /// not to do about it — is the same either way and stays where it is
157    /// written, which is also where `tools/ci/check-diagnostic-messages.py` reads
158    /// it.
159    ///
160    /// `NoMap` answers with the `areas[]` field: such a campaign has no area for
161    /// the light pass to walk, so this arm is reached only by a caller asking in
162    /// the abstract, and `areas[]` is the surface it would be writing.
163    ///
164    /// Returned BARE, with no stage qualifier, because two callers need two
165    /// different qualifiers of one name — `DW0210` says "declare …" and `DW0211`
166    /// says "Fix in …" — and a second accessor for the second phrasing would be
167    /// the duplication this module exists to remove.
168    #[must_use]
169    pub fn lighting_field(self) -> &'static str {
170        match self {
171            Self::Prefabs | Self::NoMap => "`world.areas[].lighting`",
172            Self::SitePlan => "the site plan's `lighting`",
173        }
174    }
175
176    /// Whether this campaign has the per-area **`mitigation`** surface at all.
177    ///
178    /// It lives on an `areas[]` entry, and a site-plan campaign is required to
179    /// declare an empty `areas` list (`DW0839`), so it has none. That is a
180    /// capability a derived map does not have rather than a wording question: a
181    /// darkness refusal offers the option only where it exists, because offering
182    /// it anywhere else is a prescription the campaign is refused for carrying
183    /// out.
184    #[must_use]
185    pub fn has_area_mitigation(self) -> bool {
186        !matches!(self, Self::SitePlan)
187    }
188}
189
190/// **What declares how big this campaign's map is** — asked once, by both tiers
191/// that need the answer.
192///
193/// A horizon that BUILDS terrain has to ring a stated extent, and whether one is
194/// stated is a fact about the documents: validation refuses `DW0855` on it
195/// before a block is placed, and the compiler derives the rectangle from the
196/// same answer. Two implementations of one predicate is two verdicts waiting to
197/// disagree — the tiers already disagreed once about which arm of `DW0855`'s own
198/// message applied — so the predicate lives here and the rectangle is derived
199/// from the authority this names.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum Extent {
202    /// **The site plan's `region`**: the whole map's design of record, the
203    /// brief's number flowing down. A box outside it is `DW0826`, so nothing a
204    /// part does can grow it.
205    Region,
206    /// **The one piece's own declared region**: the campaign is one area bound
207    /// to one `prefab`, so its whole map is that piece and the piece's declared
208    /// structure size is the map's extent. This is what a *site* is — a building
209    /// with its island, its moat and its banks inside one box — and the
210    /// declaration is the prefab document's, held to the `.nbt` by `DW0888`.
211    OnePiece,
212    /// **Nothing states one.** Two or more areas, or one area drawing from a
213    /// pool, or no area at all: the only candidate left is the union of what
214    /// happens to get placed, which `DW0855` refuses because areas sit on the
215    /// compiler's fixed stride and a pool's footprint is the solver's answer.
216    Unstated,
217}
218
219impl Extent {
220    /// Ask the campaign, once.
221    ///
222    /// The site plan wins where both could apply, for [`Placement::of`]'s
223    /// reason: a campaign carrying both is `DW0839`, and this describes the
224    /// authority the rest of the compiler actually uses.
225    #[must_use]
226    pub fn of(c: &Campaign) -> Self {
227        if c.site_plan.is_some() {
228            Self::Region
229        } else if c.world.content.areas.len() == 1 && c.world.content.areas[0].prefab.is_some() {
230            Self::OnePiece
231        } else {
232            Self::Unstated
233        }
234    }
235
236    /// Whether the campaign states an extent at all — the predicate `DW0855`
237    /// refuses on.
238    #[must_use]
239    pub fn is_stated(self) -> bool {
240        !matches!(self, Self::Unstated)
241    }
242}
243
244/// **Whether this campaign's anchor vocabulary can be known at all** — asked
245/// before any rule refuses a name for not being in it.
246///
247/// A [`Placement::SitePlan`] campaign's anchor names are DERIVED: a `node-` per
248/// place, a `seam-` per barred way, an `unlock-` on the openable side of a
249/// one-sided one, `spawn` for the entry, and every `stations[]` name its nodes
250/// declare — all of them read off `layout-graph.json`. With that document absent
251/// the derived set is not EMPTY, it is **unknown**, and `DW0824` is the finding:
252/// the plan embeds a graph, and there is no graph to embed.
253///
254/// Refusing an anchor reference in that state is this module's own defect,
255/// committed against itself. [`Placement::anchor_remedy`]'s `SitePlan` sentence
256/// tells the author to write one of the derived names or to declare a station on
257/// the node it belongs to; with no graph there are no places, no ways and no
258/// nodes, so **neither half of the remedy can be taken** — the pair rule this
259/// module opens with, applied to the prescription it hands out. Measured on the
260/// gallery's site-plan point with `layout-graph.json` removed: `DW0824` (the
261/// finding, one line) followed by thirteen refusals of names that are all
262/// correct — ten `DW0142`, two `DW0371`, one `DW0343` — each printing that
263/// unreachable sentence, ahead of the one line the author was there to act on.
264///
265/// So every anchor rule asks here first and stays silent, exactly as it stays
266/// silent for a `prefab_pool` whose draw the compiler has not made yet: the
267/// answer is not known at this tier. Nothing is lost by the silence — the run
268/// stops at `DW0824` either way, and every one of those names is re-judged, with
269/// the same rules, the moment the graph exists.
270///
271/// False for a prefab campaign at every state of its documents: its vocabulary
272/// is prefab metadata, which does not come from the map pipeline.
273#[must_use]
274pub fn anchor_vocabulary_unknowable(c: &Campaign) -> bool {
275    matches!(Placement::of(c), Placement::SitePlan) && c.layout_graph.is_none()
276}