Skip to main content

delvewright_dsl/
siteplan.rs

1//! **The whole owns the space and hands out boxes** (spec-0049 §4) — pipeline
2//! stage 4, the geometric embedding of the layout graph.
3//!
4//! One campaign stage document, `site-plan.json`: the whole map's design of
5//! record. It says where the region is, which plane each place stands on, what
6//! footprint each place gets, where two places connect and through what opening,
7//! what mass the whole itself owns, and which of the brief's numbers the plan is
8//! held to.
9//!
10//! Everything here is decided **upstream of any geometry**. No block exists yet;
11//! nothing in this module reads one. What is being judged is whether the plan is
12//! a plan — whether the boxes fit in the region and not in each other, whether
13//! two places that claim to connect really touch, and whether the numbers the
14//! brief fixed still hold once the boxes are drawn.
15//!
16//! # Extent flows down, and it is unrepresentable for it to flow up
17//!
18//! The reset this stage answers was caused by parts choosing their own size and
19//! the whole becoming whatever they added up to. So [`SitePlanContent::region`]
20//! is a **required field with no derived spelling**: there is no
21//! `"region": "fit"`, no default, and no constructor anywhere that computes one
22//! from the boxes. A plan cannot state its extent as a consequence; it can only
23//! state it, and `DW0826` then refuses a box that does not fit, naming the box
24//! rather than the region. That is not a check — it is the absence of a way to
25//! write the other thing.
26//!
27//! # Seams are allocated, not discovered
28//!
29//! A seam is placed by the plan, on a face the two boxes already share, at cells
30//! the plan names. Two places therefore connect **by construction**: the
31//! two-pieces-cannot-mate failure is resolved here, where both boxes are still
32//! free to move, and never later between two finished buildings. `DW0828` and
33//! `DW0829` are what make the allocation real rather than a claim.
34//!
35//! # One authority per fact
36//!
37//! Three places where the obvious shape would have carried two:
38//!
39//! * A box's floor is its **datum** and nothing else. The spec's `min` carried a
40//!   `y` beside the declared floor, which is two numbers for one plane and no
41//!   rule about which wins; here a box is a footprint (`min`/`extent` on `x`
42//!   and `z`) standing at a [`Floor`]. §9 records the departure.
43//! * A seam's **rise is derived** from the two boxes' floors. Authoring it would
44//!   be authoring arithmetic — unlike the layout graph's `critical_path`, which
45//!   is authored precisely because it is a *choice* among many, a rise is the
46//!   consequence of where the plan already put the two places. §9 records it.
47//! * The plan's `lighting` is [`crate::stages::AreaLighting`], the engine's
48//!   existing "which fixture, to what light level" object, not a twin of it.
49//!
50//! # No opt-out exists
51//!
52//! Not one check here has an acknowledgement, an override or an exemption
53//! field. That is deliberate and it is the cheapest possible answer to
54//! `CLAUDE.md`'s question of every escape hatch — *could the defect this hatch
55//! exists to catch supply the hatch's own proof obligation?* — because a hatch
56//! that does not exist cannot be supplied.
57//!
58//! Determinism (ADR-0006): every set and map is a `BTreeSet`/`BTreeMap` and
59//! every walk is over a slice in document order.
60
61use std::collections::{BTreeMap, BTreeSet};
62use std::num::NonZeroU32;
63
64use schemars::JsonSchema;
65use serde::{Deserialize, Serialize};
66
67use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
68use crate::envelope::Campaign;
69use crate::ids::{DatumId, EdgeId, FactId, NodeId, ViewId, VolumeId};
70use crate::layout::{Edge, LayoutGraphContent, StationKind};
71use crate::metrics::{
72    MAX_JUMP_RISE_16, MetricKind, MetricValue, Metrics, Pitch, Reads, SizeClass, WayClass,
73    passable_clearance_cells, passable_width_cells,
74};
75use crate::stages::AreaLighting;
76
77crate::dw_code! {
78    /// `DW0824`: the graph and the plan do not agree exactly.
79    pub const DW_PLAN_AGREEMENT: DwCode = DwCode::new("DW0824", ExitTier::Build);
80}
81
82crate::dw_code! {
83    /// `DW0825`: a box leaves the kit grid.
84    pub const DW_BOX_OFF_GRID: DwCode = DwCode::new("DW0825", ExitTier::Build);
85}
86
87crate::dw_code! {
88    /// `DW0826`: a box leaves the region.
89    pub const DW_BOX_LEAVES_REGION: DwCode = DwCode::new("DW0826", ExitTier::Build);
90}
91
92crate::dw_code! {
93    /// `DW0827`: two boxes overlap.
94    pub const DW_BOXES_OVERLAP: DwCode = DwCode::new("DW0827", ExitTier::Build);
95}
96
97crate::dw_code! {
98    /// `DW0828`: a seam is not on a shared face.
99    pub const DW_SEAM_NOT_SHARED: DwCode = DwCode::new("DW0828", ExitTier::Build);
100}
101
102/// **How many cells stand between two connected boxes: the wall they share.**
103///
104/// The one number every geometric check in this stage is written against, and
105/// the one an author has to know before the first box goes down. A box is the
106/// **play space** of a place — the cells a body can be in — so the shell is not
107/// inside it: it stands in this gap, and two places that connect leave exactly
108/// this much room for it. Boxes placed flush have no wall to cut a seam through
109/// and `DW0828` refuses them.
110///
111/// It is a constant rather than a literal because the authoring documents state
112/// it: [`PlanBox`]'s schema description carries this value, and
113/// `crates/dsl/tests/v14_site_plan.rs` asserts the exported description against
114/// this constant, so the rule a person reads and the rule the checks enforce
115/// cannot drift apart.
116pub const SHARED_FACE_GAP_CELLS: i64 = 1;
117
118crate::dw_code! {
119    /// `DW0829`: a seam's opening is not a standard, or does not fit.
120    pub const DW_SEAM_OPENING: DwCode = DwCode::new("DW0829", ExitTier::Build);
121}
122
123crate::dw_code! {
124    /// `DW0830`: a stair seam cannot be built at standard pitch.
125    pub const DW_STAIR_PITCH: DwCode = DwCode::new("DW0830", ExitTier::Build);
126}
127
128crate::dw_code! {
129    /// `DW0831`: a drop seam falls outside the drop policy.
130    pub const DW_DROP_POLICY: DwCode = DwCode::new("DW0831", ExitTier::Build);
131}
132
133crate::dw_code! {
134    /// `DW0876`: a seam does not declare a connection this engine builds
135    /// (spec-0053 §6).
136    ///
137    /// **One code, four shapes of one claim** — the claim being that this seam
138    /// states a crossing the derivation can build and the observer can measure:
139    ///
140    /// 1. it declares neither an `opening` nor a `contact`, or both;
141    /// 2. its contact's span leaves the shared face `DW0828` established;
142    /// 3. its contact's span is not **wider than the broadest standard opening**;
143    /// 4. it is a contact on a `stair`, `barred` or `vision` connection.
144    ///
145    /// They are one code rather than four because the author's next action is the
146    /// same in every case — say which kind of hand-off this is and give it a shape
147    /// the engine has — and because a seam exhibiting one of them has no crossing
148    /// for any rule below to judge. It is the shape `DW0830` already carries for a
149    /// stair ("three shapes of one claim") and `DW0829` for an opening ("two halves
150    /// of one claim that the opening is usable").
151    ///
152    /// Shape 3 is the floor that keeps the whole surface honest, and it is
153    /// **structural rather than seeded**: it is derived from the standard opening
154    /// set, so anything at or under it COULD have been a portal, and a doorway
155    /// declared a contact to dodge the standard set is refused by its own width.
156    /// That is the property `CLAUDE.md` demands of an escape hatch — the defect this
157    /// exists to catch is incapable of supplying the hatch's proof obligation.
158    pub const DW_CONTACT: DwCode = DwCode::new("DW0876", ExitTier::Build);
159}
160
161crate::dw_code! {
162    /// `DW0832`: a box violates its node's size class.
163    pub const DW_SIZE_CLASS: DwCode = DwCode::new("DW0832", ExitTier::Build);
164}
165
166crate::dw_code! {
167    /// `DW0833`: a brief identity does not hold.
168    pub const DW_IDENTITY_FALSE: DwCode = DwCode::new("DW0833", ExitTier::Build);
169}
170
171crate::dw_code! {
172    /// `DW0834`: the identity gate binds nothing. Warning — see [`identities`].
173    pub const DW_IDENTITY_EMPTY: DwCode = DwCode::new("DW0834", ExitTier::Build);
174}
175
176crate::dw_code! {
177    /// `DW0835`: a whole-owned volume enters a box.
178    pub const DW_VOLUME_IN_BOX: DwCode = DwCode::new("DW0835", ExitTier::Build);
179}
180
181crate::dw_code! {
182    /// `DW0839`: two placement authorities in one campaign — a `site-plan.json` and
183    /// a non-empty `areas[]` both present.
184    pub const DW_TWO_AUTHORITIES: DwCode = DwCode::new("DW0839", ExitTier::Build);
185}
186
187crate::dw_code! {
188    /// `DW0883`: a box is not placed exactly once (spec-0059 §5). Two shapes of one
189    /// claim: a connected component of the seam graph in which no box is pinned, so
190    /// nothing places it; and a pinned box the packing also reaches, at a different
191    /// corner, so two things place it.
192    pub const DW_UNPLACED: DwCode = DwCode::new("DW0883", ExitTier::Build);
193}
194
195// ---------------------------------------------------------------------------
196// The vocabulary the derivation synthesizes (spec-0049 §5.2)
197// ---------------------------------------------------------------------------
198
199/// **The one area a site-plan campaign has.**
200///
201/// A campaign places its pieces either with `areas[]` or with a site plan, never
202/// both (`DW0839`), so a site-plan campaign has exactly one place for an NPC to
203/// stand in and one area for a quest to belong to. The name is fixed rather than
204/// authored because there is nothing to choose: the site plan is the whole map,
205/// and a second name for it would be a second way to spell one thing.
206pub const SITE_AREA: &str = "area/site";
207
208/// The anchor name the campaign's **entry** stands under.
209///
210/// A *name*, and only a name: what makes this anchor the entry is the declared
211/// entry **role** (spec-0046) the derivation gives it, which is the one thing
212/// the compiler's resolution consults. The spelling survives because a
213/// site-plan campaign's quests and NPCs may address the entry cell like any
214/// other anchor, and `spawn` is the word the rest of the vocabulary already
215/// uses; nothing resolves through it.
216pub const ENTRY_ANCHOR: &str = "spawn";
217
218/// The anchor at a place's floor centre — where quests, NPCs and waves in a
219/// site-plan campaign stand.
220///
221/// `node/near-hall` becomes `anchor/node-near-hall`, and the reshaping is not
222/// cosmetic: a campaign reaches an anchor through [`crate::ids::AnchorId`],
223/// which is `anchor/<kebab>`, so `node/<id>` — spec-0049 §5.2's spelling — is
224/// not a name any document could write. The three families (`node-`, `seam-`,
225/// `unlock-`) are disjoint by their first segment, so no two synthesized
226/// anchors can collide however the graph is named.
227#[must_use]
228pub fn node_anchor(node: &NodeId) -> String {
229    format!("anchor/node-{}", slug(node.0.as_str()))
230}
231
232/// The gate region over a `barred` seam's opening — what an `open-gate` or a
233/// `shortcut` names.
234#[must_use]
235pub fn seam_anchor(edge: &EdgeId) -> String {
236    format!("anchor/seam-{}", slug(edge.0.as_str()))
237}
238
239/// The anchor on the openable side of a one-sided `barred` seam, where a
240/// shortcut's far-side affordance stands.
241#[must_use]
242pub fn seam_unlock_anchor(edge: &EdgeId) -> String {
243    format!("anchor/unlock-{}", slug(edge.0.as_str()))
244}
245
246/// The part of an id after its kind prefix.
247fn slug(id: &str) -> &str {
248    id.split_once('/').map_or(id, |(_, rest)| rest)
249}
250
251/// **What a sealed `barred` seam stands in until content opens it.**
252///
253/// One definition, here rather than in the derivation that lays it, for the same
254/// structural reason the metrics table owns the nav model's constants: two
255/// parties need this block and they need the *same* one. The derivation writes it
256/// into the gate region and declares it on the synthesized gate anchor; every
257/// verb that needs a gate's fill block — `close-gate`, a `shortcut`'s clear, a
258/// `timed-gate`'s clock — asks [`synthesized_gate_block`] whether this campaign
259/// declares one. A copy in each place would be an agreement rather than a fact.
260pub const SEAM_BAR: &str = "minecraft:iron_bars";
261
262/// The fill block a **synthesized** gate anchor declares, or `None` when `anchor`
263/// is not one of this campaign's derived seam gates.
264///
265/// This exists because `DW0343`'s question — *can the compiler fill and clear
266/// this gate?* — used to be answered by one instrument only, the prefab registry,
267/// and a derived world has no prefab. The answer came back honest and about a
268/// smaller world than the campaign has: a `shortcut` naming the very
269/// `anchor/seam-<edge>` the derivation seals with [`SEAM_BAR`] was refused for
270/// declaring no fill block, while the block sat in the derivation's own
271/// `AnchorSpec::Gate`. Nothing was red, because the check was refusing content.
272///
273/// `None` for a campaign with no site plan, and for any anchor the derivation
274/// does not synthesize — those are the prefab registry's to answer for, and this
275/// function never overrides it.
276#[must_use]
277pub fn synthesized_gate_block(c: &Campaign, anchor: &str) -> Option<&'static str> {
278    // Asks the ONE kind authority rather than re-walking the edges, and that is
279    // the whole repair: this used to enumerate `Edge::Barred` alone, so a
280    // `close-gate`, `shortcut` or `timed-gate` naming a **gate station**
281    // (spec-0052) would have been refused by `DW0343` for declaring no fill
282    // block — a refusal whose message says "declare the gate on an anchor of a
283    // piece an area binds", which a site-plan campaign cannot do at all
284    // (`DW0839` refuses a campaign that carries both `areas[]` and a plan).
285    // A narrow binding on the general mechanism, reading as a missing feature.
286    matches!(
287        synthesized_anchor_kinds(c).get(anchor),
288        Some(StationKind::Gate)
289    )
290    .then_some(SEAM_BAR)
291}
292
293/// **Every anchor a site-plan campaign's blockout provides, and what SHAPE each
294/// one is** — the single authority behind [`synthesized_anchors`].
295///
296/// The kind travels with the name because a kind is a property of the **anchor**,
297/// not of the verb that first needed one: `synthesized_gate_block` needed to know
298/// whether a name was a gate and answered by privately re-walking the edges, and
299/// a second consumer wanting the same fact would have re-walked them again. One
300/// function answers it, and everything that needs a shape asks here.
301///
302/// The mapping, and it is total:
303///
304/// * [`ENTRY_ANCHOR`] and every `anchor/node-…` — [`StationKind::Point`], the
305///   floor centre a body stands on.
306/// * every `anchor/unlock-…` — [`StationKind::Point`], where the shortcut's
307///   far-side affordance stands.
308/// * every `anchor/seam-…` — [`StationKind::Gate`], the region the derivation
309///   fills with [`SEAM_BAR`] and content opens.
310/// * every declared station — the kind its node declared (spec-0052 §3).
311///
312/// Empty for a campaign with no site plan — it has prefabs instead, and their
313/// metadata is the authority.
314#[must_use]
315pub fn synthesized_anchor_kinds(c: &Campaign) -> BTreeMap<String, StationKind> {
316    let mut out: BTreeMap<String, StationKind> = BTreeMap::new();
317    if c.site_plan.is_none() {
318        return out;
319    }
320    let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
321        return out; // `DW0824` refused the plan; there is nothing to name.
322    };
323    out.insert(ENTRY_ANCHOR.to_string(), StationKind::Point);
324    for n in &graph.nodes {
325        out.insert(node_anchor(&n.id), StationKind::Point);
326        // A station whose name collides with a synthesized one is `DW0869`, and
327        // one that collides with another station is `DW0870`; both are errors,
328        // so this insert never silently reinterprets a name a campaign builds
329        // with. Inserting anyway keeps the set EXACT for the refused document
330        // too, which is what lets the kind check name the declared kind rather
331        // than a shape the author did not write.
332        for s in &n.stations {
333            out.insert(s.anchor.as_str().to_string(), s.kind);
334        }
335    }
336    for e in &graph.edges {
337        let Edge::Barred { id, opens_from, .. } = e else {
338            continue;
339        };
340        out.insert(seam_anchor(id), StationKind::Gate);
341        if !matches!(opens_from, crate::layout::OpensFrom::Either) {
342            out.insert(seam_unlock_anchor(id), StationKind::Point);
343        }
344    }
345    out
346}
347
348/// **Every anchor a site-plan campaign's blockout provides**, derived from the
349/// documents alone.
350///
351/// One authority, and that is why it lives here rather than in the derivation
352/// that places them: validation resolves a campaign's anchor references against
353/// this set, the derivation creates exactly these anchors, and a name that
354/// validated could therefore never fail to exist at build time. Two functions
355/// agreeing about the spelling is the drift this one removes.
356///
357/// Empty for a campaign with no site plan — it has prefabs instead, and their
358/// metadata is the authority.
359#[must_use]
360pub fn synthesized_anchors(c: &Campaign) -> BTreeSet<String> {
361    // The names ARE the keys of the kind table, taken rather than re-derived:
362    // two functions walking the graph and agreeing about the spelling is the
363    // exact drift this module's note exists to remove, and a station made the
364    // walk long enough that a second copy would eventually diverge.
365    synthesized_anchor_kinds(c).into_keys().collect()
366}
367
368/// **The synthesized names one PLACE owes** (spec-0050 §6) — the subset of
369/// [`synthesized_anchors`] whose bearer is this box.
370///
371/// Its own `anchor/node-…`; [`ENTRY_ANCHOR`] when it is the entry node; each
372/// `anchor/unlock-…` whose `opens_from` side it is — the side the derivation
373/// stands that affordance in. A gate region (`anchor/seam-…`) is **never** owed:
374/// it is whole fabric, standing in a party plane the piece does not own.
375///
376/// Here rather than in `crate::detailplan` because the answer is a fact about
377/// the graph, and [`synthesized_anchors`] is the one authority for what a
378/// site-plan campaign provides — a second module deciding which names belong to
379/// which place is exactly the two-functions-agreeing-about-spelling drift that
380/// note exists to remove.
381///
382/// `crates/delvec/tests/blockout.rs`'s
383/// `the_owed_anchors_partition_the_synthesized_set` proves the two PARTITION
384/// rather than merely overlap: every synthesized name is owed by exactly one
385/// place or is a gate region no place owes. A name in neither would be one a
386/// campaign resolves and no piece is ever asked for; a name in both would be two
387/// pieces claiming one anchor.
388///
389/// Empty for a campaign with no site plan, and for a node the graph does not
390/// have.
391#[must_use]
392pub fn owed_anchors(c: &Campaign, node: &NodeId) -> BTreeSet<String> {
393    let mut out: BTreeSet<String> = BTreeSet::new();
394    if c.site_plan.is_none() {
395        return out;
396    }
397    let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
398        return out; // `DW0824` refused the plan; there is nothing to name.
399    };
400    let Some(n) = graph.nodes.iter().find(|n| &n.id == node) else {
401        return out; // `DW0842` names a row whose place the graph does not have.
402    };
403    out.insert(node_anchor(node));
404    if &graph.entry == node {
405        out.insert(ENTRY_ANCHOR.to_string());
406    }
407    // Every station of this node (spec-0052 §6). The owed set grows **upstream**,
408    // and that one widening is what carries the whole binding chain: the
409    // `detail-plan` `anchors` map must now bind each of them, and it still
410    // refuses every key outside this set, so a binding cannot invent vocabulary
411    // and a typo cannot pass as intent.
412    for s in &n.stations {
413        out.insert(s.anchor.as_str().to_string());
414    }
415    for e in &graph.edges {
416        let Edge::Barred { id, opens_from, .. } = e else {
417            continue;
418        };
419        let side = match opens_from {
420            crate::layout::OpensFrom::A => e.a(),
421            crate::layout::OpensFrom::B => e.b(),
422            crate::layout::OpensFrom::Either => continue,
423        };
424        if side == node {
425            out.insert(seam_unlock_anchor(id));
426        }
427    }
428    out
429}
430
431// ---------------------------------------------------------------------------
432// The document (spec-0049 §4.1)
433// ---------------------------------------------------------------------------
434
435/// The `site-plan` stage document's payload: the geometric embedding of the
436/// layout graph, and the whole map's design of record.
437#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
438#[serde(deny_unknown_fields)]
439pub struct SitePlanContent {
440    /// **The whole map's one region, in world coordinates.**
441    ///
442    /// Required, with no way to omit it and no way to derive it: the schema has
443    /// no "compute this from the boxes" spelling, so extent-flows-up is
444    /// unrepresentable rather than merely forbidden. The number comes from the
445    /// geometry brief and the identities hold the plan to it.
446    ///
447    /// The water plane is deliberately **not** site-plan surface: `horizon:
448    /// ocean` in the stage-1 world document already fixes sea level, and the
449    /// plan reads that single authority rather than restating it.
450    pub region: WorldBox,
451    /// Named ground planes the boxes stand on.
452    #[serde(default, skip_serializing_if = "Vec::is_empty")]
453    pub datums: Vec<Datum>,
454    /// **Exactly one box per graph node** (`DW0824`).
455    pub boxes: Vec<PlanBox>,
456    /// **Exactly one seam per traversal edge** (`DW0824`). A `vision` edge
457    /// carries a [`Sightline`] instead — see [`Sightline`] for why.
458    pub seams: Vec<Seam>,
459    /// The mass the WHOLE owns: the mountain a cave system is inside, the ground
460    /// under a village, the sky a silhouette needs kept empty.
461    #[serde(default, skip_serializing_if = "Vec::is_empty")]
462    pub volumes: Vec<Volume>,
463    /// The guarded comparisons binding this plan to the geometry brief's facts.
464    #[serde(default, skip_serializing_if = "Vec::is_empty")]
465    pub identities: Vec<Identity>,
466    /// One per `vision` edge (`DW0824`).
467    #[serde(default, skip_serializing_if = "Vec::is_empty")]
468    pub sightlines: Vec<Sightline>,
469    /// The named exterior vantages the walk judges the silhouette from. Optional;
470    /// a plan with zero views has that zero stated in the binding line.
471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
472    pub views: Vec<View>,
473    /// One lighting setting applied to every enclosed box, so a blockout
474    /// interior is walkable at night without per-box surface.
475    ///
476    /// **The engine's existing object**, not a twin of it: [`AreaLighting`] is
477    /// already "which fixture, to what light level", and the relight pass that
478    /// consumes it is the same pass either way. A second two-field struct here
479    /// would be the private-copy defect `CLAUDE.md` names, and it would fork the
480    /// range check the moment one of them grew a third field.
481    #[serde(default, skip_serializing_if = "Option::is_none")]
482    pub lighting: Option<AreaLighting>,
483}
484
485/// A box of world cells: its low corner and its extent, in blocks.
486///
487/// The cells are `min[i] ..= min[i] + extent[i] - 1` on each axis. The extent is
488/// [`NonZeroU32`] rather than a `u32` a check refuses: a zero-extent region is
489/// not a small region, it is a document that does not describe a volume, and the
490/// schema says so (`minimum: 1`) so the parse refuses it as an ordinary
491/// `DW0100`. One fewer diagnostic to write and one fewer to forget.
492#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
493#[serde(deny_unknown_fields)]
494pub struct WorldBox {
495    /// Low corner `[x, y, z]`, in world coordinates.
496    pub min: [i64; 3],
497    /// Extent `[dx, dy, dz]`, in blocks.
498    pub extent: [NonZeroU32; 3],
499}
500
501impl WorldBox {
502    /// The high corner (inclusive).
503    #[must_use]
504    pub fn max(&self) -> [i64; 3] {
505        [
506            self.min[0] + i64::from(self.extent[0].get()) - 1,
507            self.min[1] + i64::from(self.extent[1].get()) - 1,
508            self.min[2] + i64::from(self.extent[2].get()) - 1,
509        ]
510    }
511}
512
513/// A named ground plane a box's floor sits on.
514#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
515#[serde(deny_unknown_fields)]
516pub struct Datum {
517    /// Datum id (`datum/<kebab>`), unique within the plan.
518    pub id: DatumId,
519    /// The world `y` of the walk plane.
520    pub y: i64,
521    /// What this plane is, for a reader of the plan.
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    pub note: Option<String>,
524}
525
526/// Where a place's walk plane is.
527///
528/// Two spellings of one number, and the second is not redundant: a plane several
529/// places stand on is named once as a [`Datum`] and moved once, while a place
530/// that stands alone at its own height has no plane to name. An `identities[]`
531/// entry can only bind to a *named* one, which is the pressure toward naming.
532#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
533#[serde(rename_all = "kebab-case", deny_unknown_fields)]
534pub enum Floor {
535    /// A plane the plan names (`DW0112` if the plan declares no such datum).
536    Datum(DatumId),
537    /// A world `y` this place alone stands at.
538    Y(i64),
539}
540
541/// What closes a place overhead.
542#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
543#[serde(rename_all = "kebab-case", deny_unknown_fields)]
544pub enum Ceiling {
545    /// Cells of headroom over the walk plane. A body's feet are at the floor and
546    /// the ceiling course sits at `floor + clearance`.
547    Clearance(NonZeroU32),
548    /// A sky-open place — a courtyard, a shore, a summit.
549    ///
550    /// The plan claims the ground and its size class's own minimum clearance,
551    /// and **nothing above that**: an open place is precisely one that makes no
552    /// claim on the air over it, so a `clearance` volume above a courtyard is
553    /// the whole reserving sky rather than two authorities over one cell.
554    Open,
555}
556
557/// One place, embedded: a footprint standing on a plane.
558///
559/// **A box is a plan, not a prism.** Its `min`/`extent` are the two horizontal
560/// axes and its vertical position is [`PlanBox::floor`] — one authority for the
561/// plane, where a `y` inside `min` beside a declared floor would have been two
562/// numbers with no rule about which the derivation believes.
563///
564/// **A box is the PLAY SPACE, and connected boxes are separated by exactly one
565/// cell.** `extent` is the interior a body can stand in; the shell the blockout
566/// derivation builds is not inside it. That shell stands in the one-cell gap
567/// between two neighbours, and on the course under the floor and over the
568/// ceiling. So two places that connect are placed one cell apart on the face
569/// they share — that cell is the wall they have in common, written once — and
570/// two boxes placed flush have no wall for a seam to be cut through, which
571/// `DW0828` refuses. Worked: a box at `min: [4, 4]` with `extent: [4, 4]`
572/// occupies x 4..7, so its eastern neighbour's `min` x is 9, never 8.
573///
574/// Two consequences follow, and they are what make the checks say what they look
575/// like they say: the size-class ladder judges `extent` directly (`DW0832`),
576/// its smallest rung `4 × 4` being exactly one kit quantum; and a plan never
577/// states a wall's thickness anywhere, because the gap is where the wall is.
578#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
579#[serde(deny_unknown_fields)]
580pub struct PlanBox {
581    /// The graph node this box embeds.
582    ///
583    /// **The ordering tooth, at the type level**: there is no way to write a box
584    /// that does not name a place, so a site plan cannot describe a space the
585    /// layout graph has not declared (spec-0049 §7.1).
586    pub node: NodeId,
587    /// **A pin**: the low corner `[x, z]` in world coordinates, when the author
588    /// chooses where this box stands. Optional (spec-0059 §2): a box with no pin
589    /// stands where the packing puts it — one cell beyond the face of the box
590    /// its first seam in document order hangs it off. At least one box of every
591    /// connected component of the seam graph is pinned, or nothing places the
592    /// component (`DW0883`); a pinned box the packing also reaches at a
593    /// different corner is refused naming both (`DW0883`). Two horizontal
594    /// numbers, never three — the vertical position is `floor`.
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub min: Option<[i64; 2]>,
597    /// Interior footprint `[dx, dz]`, in blocks, on the kit grid (`DW0825`).
598    /// Two horizontal numbers, never three — the vertical size is `ceiling`.
599    ///
600    /// **This is play space, not the building.** The box covers `min` to
601    /// `min + extent - 1` inclusive, and the walls stand outside it, in the
602    /// one-cell gap that separates connected places (`DW0828`).
603    pub extent: [NonZeroU32; 2],
604    /// The walk plane.
605    pub floor: Floor,
606    /// What closes it overhead.
607    pub ceiling: Ceiling,
608    /// **The sky this place stands under from the first tick** (spec-0080
609    /// §3.2): one of `world.atmospheres[]`, painted at world setup over the
610    /// box's play space grown as far as the client's biome blend reads — the
611    /// same capability `areas[].atmosphere` is, on the
612    /// other class of place with a world box. Absent: the horizon's biome.
613    #[serde(default, skip_serializing_if = "Option::is_none")]
614    pub atmosphere: Option<crate::ids::AtmosphereId>,
615}
616
617/// Which side of a box a seam sits on.
618///
619/// The engine's existing face vocabulary — the same six names a prefab's face
620/// contract writes (`compiler::faces`), so a reader who knows one knows the
621/// other and no translation table exists to disagree with itself.
622#[derive(
623    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
624)]
625#[serde(rename_all = "kebab-case")]
626pub enum Face {
627    /// `+x`.
628    East,
629    /// `-x`.
630    West,
631    /// `+y`.
632    Up,
633    /// `-y`.
634    Down,
635    /// `+z`.
636    South,
637    /// `-z`.
638    North,
639}
640
641impl Face {
642    /// The unit vector this face points along.
643    #[must_use]
644    pub fn vector(self) -> [i64; 3] {
645        match self {
646            Face::East => [1, 0, 0],
647            Face::West => [-1, 0, 0],
648            Face::Up => [0, 1, 0],
649            Face::Down => [0, -1, 0],
650            Face::South => [0, 0, 1],
651            Face::North => [0, 0, -1],
652        }
653    }
654
655    /// True for a face whose plane is horizontal (a floor or a ceiling).
656    #[must_use]
657    pub fn is_horizontal_plane(self) -> bool {
658        matches!(self, Face::Up | Face::Down)
659    }
660
661    /// The name a refusal prints.
662    #[must_use]
663    pub fn as_str(self) -> &'static str {
664        match self {
665            Face::East => "east",
666            Face::West => "west",
667            Face::Up => "up",
668            Face::Down => "down",
669            Face::South => "south",
670            Face::North => "north",
671        }
672    }
673}
674
675/// A crossing's position on one box's face, from that box's own low corner.
676///
677/// One integer on a wall face (cells along the face's horizontal axis), a pair
678/// through a floor or ceiling (cells along `x` and `z`). Untagged, so the
679/// document writes `"at": 2` or `"at": [2, 3]`; the face decides which shape is
680/// a position and the other is `DW0828`.
681#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
682#[serde(untagged)]
683pub enum Offset {
684    /// Cells along a vertical face's horizontal axis.
685    Along(i64),
686    /// Cells along `x` and `z` on a horizontal face.
687    Plane([i64; 2]),
688}
689
690/// One traversal edge, allocated: an opening on a face the two boxes share.
691///
692/// The seam carries **no rise** and **no sill**. A rise is `floor(b) − floor(a)`, which the plan
693/// has already stated by putting the two places where it put them; a second
694/// declaration of it could only ever agree or be a refusal teaching nothing the
695/// datums did not already say. `DW0830` and `DW0831` judge the derived number,
696/// and the stage-5 observer of the built bytes judges the realized one against
697/// the same derivation rather than against an author's copy of it.
698#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
699#[serde(deny_unknown_fields)]
700pub struct Seam {
701    /// The graph edge this seam allocates.
702    pub edge: EdgeId,
703    /// Which face **of the edge's `a` box** the seam sits on. The `b` box is the
704    /// neighbour across it (`DW0828`).
705    pub face: Face,
706    /// Where the crossing sits on **`a`'s** face: an offset from `a`'s own low
707    /// corner along the face, never a world coordinate (spec-0059 §2).
708    ///
709    /// * on a vertical face (`east`/`west`/`north`/`south`) — one integer, cells
710    ///   along the face's horizontal axis (`z` for east/west, `x` for
711    ///   north/south). The **sill is not written**: it is
712    ///   `max(floor(a), floor(b))`, which the plan has already stated.
713    /// * on a horizontal face (`up`/`down`) — `[dx, dz]`, cells along `x` and
714    ///   `z`.
715    ///
716    /// Omitted, the crossing is **centred** on the face: `(extent - width) div
717    /// 2`. An offset that leaves the face, or of the wrong shape for the face,
718    /// is `DW0828`.
719    #[serde(default, skip_serializing_if = "Option::is_none")]
720    pub at: Option<Offset>,
721    /// Where the same crossing sits on **`b`'s** face — the same offset, taken
722    /// from `b`'s own low corner. Omitted, centred on `b`'s face. The packing
723    /// places `b` so that the two agree: `corner(b) = corner(a) + at - meets`
724    /// along the face (spec-0059 §3). On a seam whose two boxes both already
725    /// stand, the two must name the same cells (`DW0828`).
726    #[serde(default, skip_serializing_if = "Option::is_none")]
727    pub meets: Option<Offset>,
728    /// **A PORTAL**: a named opening from the metrics table's standard set
729    /// (`DW0812` on a name the table does not define, `DW0829` on one that does
730    /// not fit). A body crosses at exactly the cells `at` and this standard
731    /// allocate.
732    ///
733    /// **Exactly one of this and [`Seam::contact`]** (`DW0876`).
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub opening: Option<String>,
736    /// **A CONTACT**: the two places simply meet along a front, rather than
737    /// through a doorway (spec-0053 §4).
738    ///
739    /// **Exactly one of this and [`Seam::opening`]** (`DW0876`).
740    ///
741    /// The width of a front where two places meet is a fact of those two boxes'
742    /// shared face — per-campaign geometry, continuous — so it is never a named
743    /// standard. A table that enumerated it would gain a new entry per campaign,
744    /// which is the size ladder's own failure mode reproduced in the opening
745    /// set: an `opening.gate-front` of 21×4 is content wearing a standard's
746    /// clothes (spec-0053 §7).
747    #[serde(default, skip_serializing_if = "Option::is_none")]
748    pub contact: Option<Contact>,
749    /// Which of the edge's two boxes hosts the stair massing. Required on a
750    /// `stair` edge and refused on any other (`DW0830`, `DW0824`).
751    #[serde(default, skip_serializing_if = "Option::is_none")]
752    pub stair_in: Option<NodeId>,
753}
754
755/// A mass the whole itself owns.
756#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
757#[serde(deny_unknown_fields)]
758pub struct Volume {
759    /// Volume id (`volume/<kebab>`), unique within the plan.
760    pub id: VolumeId,
761    /// The cells it covers.
762    pub region: WorldBox,
763    /// What the whole is doing with them.
764    pub role: VolumeRole,
765    /// What this mass is, for a reader of the plan.
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub note: Option<String>,
768}
769
770/// What a whole-owned volume is for.
771#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
772#[serde(rename_all = "kebab-case")]
773pub enum VolumeRole {
774    /// Solid mass the places are cut into — the mountain around a cave system.
775    Massif,
776    /// The ground the places stand on.
777    Ground,
778    /// Air the whole keeps empty — the sky a silhouette needs, the drop a
779    /// vista looks over.
780    Clearance,
781}
782
783/// One guarded comparison binding the plan to a fact of the written brief.
784#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
785#[serde(deny_unknown_fields)]
786pub struct Identity {
787    /// The brief fact this holds the plan to (`DW0112` if the brief has no such
788    /// fact).
789    pub fact: FactId,
790    /// What is measured off the plan.
791    pub measure: Measure,
792    /// How the measurement must stand to the fact's value.
793    pub cmp: Cmp,
794}
795
796/// What an identity measures off the plan.
797///
798/// A small **fixed** vocabulary, spelled as a tagged union rather than as the
799/// spec's `box(<node>).extent.x` string. The vocabulary is exactly the spec's
800/// five; what changes is that a measure is parsed by serde instead of by a
801/// grammar this module would have had to write, own and document — so an
802/// unknown measure is an ordinary `DW0100`, a node it names is checked like
803/// every other reference, and the growth the spec's marked judgement predicts is
804/// a variant rather than a second escaping rule. §9 records the departure.
805#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
806#[serde(tag = "of", rename_all = "kebab-case", deny_unknown_fields)]
807pub enum Measure {
808    /// The whole region's extent on one axis, in blocks.
809    RegionExtent {
810        /// Which axis.
811        axis: Axis,
812    },
813    /// One place's footprint on one horizontal axis, in blocks.
814    BoxExtent {
815        /// The place.
816        node: NodeId,
817        /// Which horizontal axis.
818        axis: PlanAxis,
819    },
820    /// One place's headroom over its walk plane, in blocks. A sky-open place
821    /// measures its size class's own minimum clearance — the least air the
822    /// ladder says such a place has.
823    BoxHeight {
824        /// The place.
825        node: NodeId,
826    },
827    /// The horizontal distance between two places' footprint centres, in blocks.
828    /// Euclidean on `x`/`z`; the standoff a brief states between two things.
829    DistanceXz {
830        /// One place.
831        from: NodeId,
832        /// The other.
833        to: NodeId,
834    },
835    /// A named ground plane's world `y`.
836    DatumY {
837        /// The plane.
838        datum: DatumId,
839    },
840}
841
842/// One of the three world axes.
843#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
844#[serde(rename_all = "kebab-case")]
845pub enum Axis {
846    /// East–west.
847    X,
848    /// Up–down.
849    Y,
850    /// North–south.
851    Z,
852}
853
854/// One of the two horizontal axes.
855///
856/// A separate type from [`Axis`] rather than a `y` some check refuses: a box is
857/// a footprint, so its extent has no `y` to ask about, and an unrepresentable
858/// state needs no diagnostic to police it.
859#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
860#[serde(rename_all = "kebab-case")]
861pub enum PlanAxis {
862    /// East–west.
863    X,
864    /// North–south.
865    Z,
866}
867
868/// How a measurement must stand to its fact's value.
869#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
870#[serde(rename_all = "kebab-case")]
871pub enum Cmp {
872    /// Exactly.
873    Eq,
874    /// Strictly under.
875    Lt,
876    /// At most.
877    Le,
878    /// Strictly over.
879    Gt,
880    /// At least.
881    Ge,
882}
883
884impl Cmp {
885    fn holds(self, measured: f64, fact: f64) -> bool {
886        match self {
887            Cmp::Eq => (measured - fact).abs() < 1e-9,
888            Cmp::Lt => measured < fact,
889            Cmp::Le => measured <= fact,
890            Cmp::Gt => measured > fact,
891            Cmp::Ge => measured >= fact,
892        }
893    }
894
895    fn as_str(self) -> &'static str {
896        match self {
897            Cmp::Eq => "exactly",
898            Cmp::Lt => "under",
899            Cmp::Le => "at most",
900            Cmp::Gt => "over",
901            Cmp::Ge => "at least",
902        }
903    }
904}
905
906/// A `vision` edge, embedded: the segment the stage-5 battery walks.
907///
908/// A vision edge gets a sightline rather than a seam because a vista's two ends
909/// are routinely not adjacent — a bell tower seen from a shore shares no face
910/// with it — so the seam construct cannot state the one thing a vision edge
911/// asserts. A window between neighbours is simply a short sightline
912/// (spec-0049 §4.4).
913#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
914#[serde(deny_unknown_fields)]
915pub struct Sightline {
916    /// The `vision` edge this embeds.
917    pub edge: EdgeId,
918    /// The eye end, in world coordinates — inside the edge's `a` box
919    /// (`DW0824`).
920    pub from: [i64; 3],
921    /// The seen end — inside the edge's `b` box (`DW0824`).
922    pub to: [i64; 3],
923}
924
925/// A named exterior vantage the walk judges the silhouette from.
926#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
927#[serde(deny_unknown_fields)]
928pub struct View {
929    /// View id (`view/<kebab>`), unique within the plan.
930    pub id: ViewId,
931    /// Where the eye stands, in world coordinates.
932    pub eye: [i64; 3],
933    /// What it looks at.
934    pub look_at: [i64; 3],
935    /// What this view is for.
936    #[serde(default, skip_serializing_if = "Option::is_none")]
937    pub note: Option<String>,
938}
939
940// ---------------------------------------------------------------------------
941// Resolution — the plan read once, so no check re-derives a number
942// ---------------------------------------------------------------------------
943
944/// One box with everything the checks below need already worked out.
945///
946/// **The model every geometric check depends on** is stated where an author
947/// reads it — on [`PlanBox`], whose schema description carries it — and the
948/// number itself is [`SHARED_FACE_GAP_CELLS`]. In short: a box is the **play
949/// space** of a place, the shell stands in the one-cell gap between two
950/// neighbours, `extent` is therefore the interior footprint the size-class
951/// ladder judges directly (`DW0832`), and two connected places sit exactly
952/// [`SHARED_FACE_GAP_CELLS`] apart on the face they share (`DW0828`).
953#[derive(Debug, Clone)]
954struct Placed<'a> {
955    index: usize,
956    plan: &'a PlanBox,
957    /// Footprint, inclusive: `[x0, x1, z0, z1]`.
958    foot: [i64; 4],
959    /// The walk plane.
960    floor: i64,
961    /// Cells of headroom over the walk plane, or `None` when the place is
962    /// sky-open and its classification did not resolve.
963    clearance: Option<u32>,
964    /// How the place is classified, when the name resolved.
965    class: Option<PlaceClass>,
966    /// How its corner was obtained (spec-0059 §3).
967    by: Provenance,
968}
969
970/// **How a place is classified** — the two kinds of standard a box is judged
971/// against (spec-0053 §3).
972///
973/// The classification belongs to the PLACE, so it is one field of two kinds
974/// rather than two fields. Written as a second `Option<WayClass>` beside the
975/// first, every consumer would have had to remember to look at both, and the
976/// one that forgot would silently judge a road against nothing — which is the
977/// state this whole surface exists to end, reintroduced one layer down.
978#[derive(Debug, Clone, Copy, PartialEq, Eq)]
979enum PlaceClass {
980    /// A rung of the size ladder: both horizontal extents bounded.
981    Size(SizeClass),
982    /// A way: the cross-section bounded, the run free.
983    Way(WayClass),
984}
985
986impl PlaceClass {
987    /// The least interior clearance the class demands.
988    ///
989    /// The one question both kinds answer identically, which is why a sky-open
990    /// box needs no arm: an open place claims exactly its class's minimum
991    /// headroom and nothing above it, and that sentence is true of a road as it
992    /// is of a hall.
993    fn min_clearance(self) -> u32 {
994        match self {
995            PlaceClass::Size(c) => c.min_clearance,
996            PlaceClass::Way(w) => w.min_clearance,
997        }
998    }
999}
1000
1001impl Placed<'_> {
1002    fn x0(&self) -> i64 {
1003        self.foot[0]
1004    }
1005    fn x1(&self) -> i64 {
1006        self.foot[1]
1007    }
1008    fn z0(&self) -> i64 {
1009        self.foot[2]
1010    }
1011    fn z1(&self) -> i64 {
1012        self.foot[3]
1013    }
1014
1015    /// The inclusive vertical span of the play space, when it is bounded.
1016    fn y_span(&self) -> Option<(i64, i64)> {
1017        let c = i64::from(self.clearance?);
1018        Some((self.floor, self.floor + c - 1))
1019    }
1020
1021    /// The centre of the footprint, in blocks.
1022    fn centre_xz(&self) -> (f64, f64) {
1023        (
1024            (self.x0() as f64 + self.x1() as f64) / 2.0,
1025            (self.z0() as f64 + self.z1() as f64) / 2.0,
1026        )
1027    }
1028}
1029
1030// ---------------------------------------------------------------------------
1031// The resolved plan, in world cells — ONE authority, three readers
1032// ---------------------------------------------------------------------------
1033
1034/// One place, resolved into world cells: the play space the plan gives it.
1035///
1036/// Public because three readers need the same answer and two of them are in
1037/// another crate: the stage-4 checks here, the **blockout derivation** that
1038/// builds the mass, and the **stage-5 battery** that judges the built bytes
1039/// against the plan. Two of those computing "where is this box" independently is
1040/// how a builder and its observer come to agree about a world neither of them
1041/// describes.
1042#[derive(Debug, Clone, PartialEq, Eq)]
1043pub struct PlacedBox {
1044    /// The place this embeds.
1045    pub node: NodeId,
1046    /// Inclusive footprint `[x0, x1, z0, z1]`.
1047    pub foot: [i64; 4],
1048    /// The walk plane's world `y`.
1049    pub floor: i64,
1050    /// Cells of headroom over the walk plane.
1051    pub clearance: u32,
1052    /// True when the plan declared no ceiling — a courtyard, a shore, a summit.
1053    /// The place still claims its size class's own minimum headroom (which is
1054    /// what [`PlacedBox::clearance`] holds); what it makes no claim on is the
1055    /// air above that.
1056    pub open: bool,
1057}
1058
1059impl PlacedBox {
1060    /// The play space's inclusive world AABB.
1061    #[must_use]
1062    pub fn space(&self) -> ([i64; 3], [i64; 3]) {
1063        (
1064            [self.foot[0], self.floor, self.foot[2]],
1065            [
1066                self.foot[1],
1067                self.floor + i64::from(self.clearance) - 1,
1068                self.foot[3],
1069            ],
1070        )
1071    }
1072
1073    /// The floor centre — where a body seated in this place stands.
1074    #[must_use]
1075    pub fn centre(&self) -> [i64; 3] {
1076        [
1077            (self.foot[0] + self.foot[1]) / 2,
1078            self.floor,
1079            (self.foot[2] + self.foot[3]) / 2,
1080        ]
1081    }
1082}
1083
1084/// One connection, resolved into world cells: the wall the two places share and
1085/// the hole the plan cut in it.
1086#[derive(Debug, Clone, PartialEq, Eq)]
1087pub struct PlacedSeam {
1088    /// The connection this allocates.
1089    pub edge: EdgeId,
1090    /// Its class, as the graph spells it.
1091    pub class: &'static str,
1092    /// The `a` end.
1093    pub a: NodeId,
1094    /// The `b` end.
1095    pub b: NodeId,
1096    /// Which face **of `a`** the seam sits on.
1097    pub face: Face,
1098    /// The axis the shared wall is flat in: 0 = x, 1 = y, 2 = z.
1099    pub normal_axis: usize,
1100    /// The wall's coordinate on that axis — one cell thick, so one number.
1101    pub plane: i64,
1102    /// The opening's inclusive world AABB (flat in [`Self::normal_axis`]).
1103    pub opening: ([i64; 3], [i64; 3]),
1104    /// The whole rectangle the two boxes share on that wall, inclusive.
1105    pub shared: ([i64; 3], [i64; 3]),
1106    /// Which kind of connection this seam allocates (spec-0053 §4). A portal's
1107    /// `opening` is a standard's rectangle; a contact's is its span.
1108    pub crossing: Crossing,
1109    /// `floor(b) − floor(a)`, derived — never authored (see [`Seam`]).
1110    pub rise: i64,
1111    /// Which place hosts the stair massing, on a `stair`.
1112    pub stair_in: Option<NodeId>,
1113}
1114
1115/// The plan's boxes, resolved by the code the stage-4 checks judge with.
1116///
1117/// A box whose floor names an undeclared datum is **absent** — `DW0112` has
1118/// refused it, and a place with no plane has no cells for any reader to work in.
1119/// A sky-open box whose size class did not resolve is absent for the same reason
1120/// (`DW0812` refused the class, so the plan states no headroom for it at all).
1121#[must_use]
1122pub fn placed_boxes(c: &Campaign, reads: &mut Reads) -> Vec<PlacedBox> {
1123    let (Some(plan), Some(graph)) = (
1124        c.site_plan.as_ref().map(|p| &p.content),
1125        c.layout_graph.as_ref().map(|g| &g.content),
1126    ) else {
1127        return Vec::new();
1128    };
1129    let table = Metrics::table();
1130    let mut sink = Vec::new();
1131    resolve(plan, graph, &table, reads, &mut sink)
1132        .0
1133        .into_iter()
1134        .filter_map(|p| {
1135            Some(PlacedBox {
1136                node: p.plan.node.clone(),
1137                foot: p.foot,
1138                floor: p.floor,
1139                clearance: p.clearance?,
1140                open: matches!(p.plan.ceiling, Ceiling::Open),
1141            })
1142        })
1143        .collect()
1144}
1145
1146/// **The one place a seam's crossing rectangle is computed**, for either kind
1147/// of connection (spec-0053 §4).
1148///
1149/// A portal's rectangle is the named standard's `width × height` anchored at
1150/// `at`. A contact's is its span: `at` plus the declared `extent`, or `at` to
1151/// the far edge of the shared face when no extent is declared.
1152///
1153/// One function rather than one per kind, and one call rather than a copy in
1154/// each reader, because this rectangle is simultaneously the derivation's carve,
1155/// `DW0836`'s allocation, `DW0838`'s allocation set and `DW0877`'s span. Two
1156/// implementations of it would be a plan-time green and a byte-time green about
1157/// two different rectangles, which is the defect `shared_face` already has one
1158/// implementation to prevent.
1159///
1160/// `None` when the seam names an opening the table does not define — `DW0812`
1161/// refused it and there is no rectangle to build or measure.
1162fn crossing_rect(
1163    s: &Seam,
1164    at: [i64; 2],
1165    face: &SharedFace,
1166    table: &Metrics,
1167    reads: &mut Reads,
1168) -> Option<(Crossing, [i64; 2])> {
1169    if s.contact.is_some() {
1170        return Some((Crossing::Contact, contact_extent(s, at, face)));
1171    }
1172    let named = s.opening.as_ref()?;
1173    let entry = table.resolve(MetricKind::Opening, named).ok()?;
1174    match entry.value(reads) {
1175        MetricValue::Opening(o) => {
1176            Some((Crossing::Portal, [i64::from(o.width), i64::from(o.height)]))
1177        }
1178        _ => None,
1179    }
1180}
1181
1182/// **How big a contact's span is** — the one authority, read by
1183/// [`crossing_rect`] and by the refusal that judges it (`DW0876`).
1184///
1185/// A declared `extent` is taken as written. With none declared the span runs
1186/// from `at` to the far edge of the shared face on both axes, which is how a
1187/// contact along the whole of a face is spelled. An `at` already past that edge
1188/// would give a negative extent, so it is clamped to one cell: the rectangle
1189/// stays well-formed and `DW0876` describes it, rather than the arithmetic
1190/// producing a rectangle nothing downstream could reason about.
1191fn contact_extent(s: &Seam, at: [i64; 2], face: &SharedFace) -> [i64; 2] {
1192    match s.contact.as_ref().and_then(|c| c.extent) {
1193        Some(e) => [i64::from(e[0].get()), i64::from(e[1].get())],
1194        None => [(face.u.1 - at[0] + 1).max(1), (face.v.1 - at[1] + 1).max(1)],
1195    }
1196}
1197
1198/// Which axis a face is flat in: 0 = x, 1 = y, 2 = z.
1199#[must_use]
1200pub fn normal_axis_of(face: Face) -> usize {
1201    match face {
1202        Face::East | Face::West => 0,
1203        Face::Up | Face::Down => 1,
1204        Face::South | Face::North => 2,
1205    }
1206}
1207
1208/// A seam's crossing rectangle as an inclusive world AABB, flat in the face's
1209/// normal axis — [`crossing_rect`]'s two numbers put where the world is.
1210///
1211/// Extracted rather than written twice because [`stair_run`] needs the same
1212/// rectangle at validation tier, before any `PlacedSeam` exists.
1213fn crossing_aabb(
1214    s: &Seam,
1215    at: [i64; 2],
1216    face: &SharedFace,
1217    extent: [i64; 2],
1218) -> ([i64; 3], [i64; 3]) {
1219    let normal_axis = normal_axis_of(s.face);
1220    let (u_axis, v_axis) = in_plane_axes(s.face);
1221    let mut lo = [0i64; 3];
1222    let mut hi = [0i64; 3];
1223    lo[normal_axis] = face.plane;
1224    hi[normal_axis] = face.plane;
1225    lo[u_axis] = at[0];
1226    hi[u_axis] = at[0] + extent[0] - 1;
1227    lo[v_axis] = at[1];
1228    hi[v_axis] = at[1] + extent[1] - 1;
1229    (lo, hi)
1230}
1231
1232/// **What a stair costs the box that hosts it** — the one place the geometry of
1233/// a run is worked out, for both the check that refuses a plan (`DW0830`) and
1234/// the derivation that lays the treads.
1235///
1236/// # Why this is one function and not two
1237///
1238/// It was two, and the two disagreed. `DW0830` measured the run against the
1239/// seam's **rise** and against the host's whole **extent**; the derivation
1240/// measures it against the **climb** the opening really asks for and against
1241/// the run the host really has beside the hole. Both readings are defensible
1242/// in isolation and neither is the other, so a plan could pass the check by one
1243/// arithmetic and be refused by the other at build time — the treads then went
1244/// unlaid, and the place they were the only way into came back as `DW0837` with
1245/// nothing pointing at the stair. Found by building: a two-place plan whose
1246/// lower room is entered through a hole in the upper room's floor reached green
1247/// at stage 4 with `needs 8, affords 8` and built no stair at all, because the
1248/// climb to the pierced plane is 7 and the run beside a centred hole is 6.
1249///
1250/// So the rule lives here and both readers call it. A plan that reaches green
1251/// is a plan the derivation can build, by construction rather than by two
1252/// arithmetics agreeing.
1253///
1254/// # What the numbers mean
1255///
1256/// * `climb` — how high the courses must carry a body. Across a **vertical**
1257///   face that is the seam's own sill, because the body stands at the sill and
1258///   steps through; through a **floor or ceiling** it is the pierced plane,
1259///   because the body stands in the hole and steps out beside it. Both come out
1260///   as the floor difference on an ordinary plan and differ exactly where the
1261///   plan puts the sill somewhere other than the far floor.
1262/// * `run_axis` — the horizontal axis the run is spent on. Across a vertical
1263///   face it is that face's normal; through a floor or ceiling the run may go
1264///   either way, so it is the host's longer horizontal axis.
1265/// * `start`/`step` — the stair arrives AT its seam, so course 0 is the one the
1266///   body steps off and the run walks back into the room.
1267/// * `available` — how many cells of that walk the host really has. Across a
1268///   vertical face the run walks the whole footprint. Through a floor or a
1269///   ceiling it leaves along **one** side of the hole, so it is the room on the
1270///   roomier side plus the hole's own width — never the host's whole extent,
1271///   which is a run that only exists if the stair could run both ways at once.
1272///
1273/// `None` where there is no run to lay or judge: a host at or above what the
1274/// stair has to reach (the plan named the higher place, which `DW0830` refuses
1275/// by name), or a hole that is not over this host at all (`DW0828`'s finding).
1276#[must_use]
1277pub fn stair_run(
1278    host_floor: i64,
1279    host_foot: [i64; 4],
1280    normal_axis: usize,
1281    plane: i64,
1282    opening: ([i64; 3], [i64; 3]),
1283) -> Option<StairRun> {
1284    let (olo, ohi) = opening;
1285    let lo = [host_foot[0], host_floor, host_foot[2]];
1286    let hi = [host_foot[1], host_floor, host_foot[3]];
1287    let target = if normal_axis == 1 { plane } else { olo[1] };
1288    let climb = target - host_floor;
1289    if climb <= 0 {
1290        return None;
1291    }
1292    let run_axis = if normal_axis == 1 {
1293        let ex = host_foot[1] - host_foot[0] + 1;
1294        let ez = host_foot[3] - host_foot[2] + 1;
1295        if ex >= ez { 0usize } else { 2 }
1296    } else {
1297        normal_axis
1298    };
1299    let (start, step, available) = if normal_axis == 1 {
1300        let (olo_r, ohi_r) = (
1301            olo[run_axis].max(lo[run_axis]),
1302            ohi[run_axis].min(hi[run_axis]),
1303        );
1304        if olo_r > ohi_r {
1305            return None;
1306        }
1307        let width = ohi_r - olo_r + 1;
1308        let room_lo = olo_r - lo[run_axis];
1309        let room_hi = hi[run_axis] - ohi_r;
1310        if room_lo >= room_hi {
1311            (ohi_r, -1, room_lo + width)
1312        } else {
1313            (olo_r, 1, room_hi + width)
1314        }
1315    } else if plane > hi[run_axis] {
1316        (hi[run_axis], -1, hi[run_axis] - lo[run_axis] + 1)
1317    } else {
1318        (lo[run_axis], 1, hi[run_axis] - lo[run_axis] + 1)
1319    };
1320    Some(StairRun {
1321        climb,
1322        run_axis,
1323        start,
1324        step,
1325        available,
1326    })
1327}
1328
1329/// What a stair's treads cost their host — see [`stair_run`].
1330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1331pub struct StairRun {
1332    /// How high the courses must carry a body, off the host's own walk plane.
1333    pub climb: i64,
1334    /// The horizontal axis the run walks: 0 = x, 2 = z.
1335    pub run_axis: usize,
1336    /// Where course 0 — the one the body steps off — stands on `run_axis`.
1337    pub start: i64,
1338    /// Which way the run walks back into the host: `+1` or `-1`.
1339    pub step: i64,
1340    /// How many cells of that walk the host really affords.
1341    pub available: i64,
1342}
1343
1344/// The run one standard pitch costs a climb, rounded up: a course is a whole
1345/// cell, and a run two-thirds of a cell short is a run the host does not have.
1346#[must_use]
1347pub fn run_of(pitch: &Pitch, climb: i64) -> i64 {
1348    let (span, per) = (climb.abs() * i64::from(pitch.run), i64::from(pitch.rise));
1349    if per == 0 {
1350        span
1351    } else {
1352        span / per + i64::from(span % per != 0)
1353    }
1354}
1355
1356/// **The gentlest standard pitch a climb fits in `available` cells of run**, or
1357/// `None` when the table defines none that does.
1358///
1359/// The walk is over [`Metrics::names_of`] in table order — gentlest first — and
1360/// takes the first that fits. One authority, so the verdict `DW0830` reaches and
1361/// the geometry the derivation lays cannot be about different standards.
1362#[must_use]
1363pub fn gentlest_pitch(
1364    table: &Metrics,
1365    reads: &mut Reads,
1366    climb: i64,
1367    available: i64,
1368) -> Option<Pitch> {
1369    for name in table.names_of(MetricKind::Pitch) {
1370        let Ok(entry) = table.resolve(MetricKind::Pitch, name) else {
1371            continue;
1372        };
1373        let MetricValue::Pitch(p) = entry.value(reads) else {
1374            continue;
1375        };
1376        if p.rise == 0 {
1377            continue;
1378        }
1379        if run_of(p, climb) <= available {
1380            return Some(*p);
1381        }
1382    }
1383    None
1384}
1385
1386/// The standard pitch that costs a climb the LEAST run, and what that run is —
1387/// the number `DW0830`'s refusal quotes, because it is the shortest run any
1388/// standard could do the climb in.
1389#[must_use]
1390pub fn tightest_pitch(
1391    table: &Metrics,
1392    reads: &mut Reads,
1393    climb: i64,
1394) -> Option<(&'static str, i64)> {
1395    let mut best: Option<(&'static str, i64)> = None;
1396    for name in table.names_of(MetricKind::Pitch) {
1397        let Ok(entry) = table.resolve(MetricKind::Pitch, name) else {
1398            continue;
1399        };
1400        let MetricValue::Pitch(p) = entry.value(reads) else {
1401            continue;
1402        };
1403        if p.rise == 0 {
1404            continue;
1405        }
1406        let needed = run_of(p, climb);
1407        if best.is_none_or(|(_, b)| needed < b) {
1408            best = Some((name, needed));
1409        }
1410    }
1411    best
1412}
1413
1414/// The plan's seams, resolved by the code the stage-4 checks judge with.
1415///
1416/// A seam whose face the two boxes do not share, or whose opening the table does
1417/// not define, is **absent**: `DW0828`/`DW0812` refused it, and there is no hole
1418/// for a reader to build or measure.
1419#[must_use]
1420pub fn placed_seams(c: &Campaign, boxes: &[PlacedBox], reads: &mut Reads) -> Vec<PlacedSeam> {
1421    let (Some(plan), Some(graph)) = (
1422        c.site_plan.as_ref().map(|p| &p.content),
1423        c.layout_graph.as_ref().map(|g| &g.content),
1424    ) else {
1425        return Vec::new();
1426    };
1427    let table = Metrics::table();
1428    let by_node: BTreeMap<&str, &PlacedBox> =
1429        boxes.iter().map(|b| (b.node.0.as_str(), b)).collect();
1430    let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
1431    // The seam anchors come from the same packing that placed `boxes`: one
1432    // arithmetic, so the derivation and its observer cannot disagree about
1433    // where a hole is.
1434    let mut sink = Vec::new();
1435    let (_, packed) = resolve(plan, graph, &table, reads, &mut sink);
1436    let mut out = Vec::new();
1437    for (i, s) in plan.seams.iter().enumerate() {
1438        let Some(at) = packed.seam_at[i] else {
1439            continue; // the packing refused or could not place this seam
1440        };
1441        let Some(edge) = edges.get(s.edge.0.as_str()) else {
1442            continue;
1443        };
1444        if !edge.has_seam() {
1445            continue;
1446        }
1447        let (Some(a), Some(b)) = (
1448            by_node.get(edge.a().0.as_str()).copied(),
1449            by_node.get(edge.b().0.as_str()).copied(),
1450        ) else {
1451            continue;
1452        };
1453        let Ok(face) = shared_face_of(a, b, s.face) else {
1454            continue;
1455        };
1456        let Some((crossing, extent)) = crossing_rect(s, at, &face, &table, reads) else {
1457            continue;
1458        };
1459        let normal_axis = normal_axis_of(s.face);
1460        // The face's two in-plane axes, in the order `at` names them.
1461        let (u_axis, v_axis) = in_plane_axes(s.face);
1462        let (lo, hi) = crossing_aabb(s, at, &face, extent);
1463        let mut smin = [0i64; 3];
1464        let mut smax = [0i64; 3];
1465        smin[normal_axis] = face.plane;
1466        smax[normal_axis] = face.plane;
1467        smin[u_axis] = face.u.0;
1468        smax[u_axis] = face.u.1;
1469        smin[v_axis] = face.v.0;
1470        smax[v_axis] = face.v.1;
1471        out.push(PlacedSeam {
1472            edge: s.edge.clone(),
1473            class: edge.class(),
1474            a: edge.a().clone(),
1475            b: edge.b().clone(),
1476            face: s.face,
1477            normal_axis,
1478            plane: face.plane,
1479            opening: (lo, hi),
1480            shared: (smin, smax),
1481            crossing,
1482            rise: b.floor - a.floor,
1483            stair_in: s.stair_in.clone(),
1484        });
1485    }
1486    out
1487}
1488
1489/// **A contact**: the span of a shared face along which two places simply meet
1490/// (spec-0053 §4).
1491///
1492/// # What a contact MEANS
1493///
1494/// The boundary is continuous ground. The derivation writes **no wall along the
1495/// span** — and wall as ever outside it — and crossing is legitimate anywhere
1496/// along it the step rule admits. It is not a wide door: `DW0829`'s standard-name
1497/// resolution and sill rule are portal checks and do not apply, because a
1498/// contact has no opening name to resolve and no single sill. Calling a 55-cell
1499/// front a door would make every downstream door check wrong.
1500///
1501/// # What the author allocates and what the engine measures
1502///
1503/// The author allocates **where** the places meet. The engine measures the
1504/// **crossing profile** from assembled bytes — which columns of the span a body
1505/// actually crosses under the step rule — and `DW0877` refuses a contact nothing
1506/// can cross. *"This face is fine"* is never a declaration this engine accepts.
1507///
1508/// Seams stay **allocated, never discovered**: the span is the edge's allocation
1509/// set for `DW0838`, so a crossing outside it is still a refusal.
1510#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1511#[serde(deny_unknown_fields)]
1512pub struct Contact {
1513    /// The span, `[u, v]` in cells on the face's own two in-plane axes, anchored
1514    /// at the seam's `at`.
1515    ///
1516    /// **Omitted, the span runs from `at` to the far edge of the shared face on
1517    /// both axes** — which is how a contact along the whole of a face is
1518    /// written, by putting `at` at the face's low corner. `DW0828`'s refusal
1519    /// prints that corner, so the number an author needs is in the message they
1520    /// would already be reading.
1521    ///
1522    /// There is no `width` standard and no `length` here, and both absences are
1523    /// the design (spec-0053 §7).
1524    #[serde(default, skip_serializing_if = "Option::is_none")]
1525    pub extent: Option<[NonZeroU32; 2]>,
1526}
1527
1528/// **Which kind of connection a seam allocates** (spec-0053 §4).
1529///
1530/// Carried on the resolved seam rather than re-derived from the authored one at
1531/// each reader, so that the derivation and the byte observer cannot disagree
1532/// about which kind a seam is — the same reason `shared_face` has one
1533/// implementation.
1534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1535#[serde(rename_all = "kebab-case")]
1536pub enum Crossing {
1537    /// A standard opening. Every allocated cell must be passable (`DW0836`).
1538    Portal,
1539    /// A front where two places meet. No wall along the span, and **at least
1540    /// one** passable column of body width somewhere in it (`DW0877`) — not
1541    /// every cell, because a contact is ground rather than a hole and the
1542    /// massing standing on it is content.
1543    Contact,
1544}
1545
1546/// The two world axes a face's `at` names, in that order.
1547fn in_plane_axes(face: Face) -> (usize, usize) {
1548    match face {
1549        // `[along, y]` — `z` for east/west, `x` for north/south.
1550        Face::East | Face::West => (2, 1),
1551        Face::South | Face::North => (0, 1),
1552        // `[x, z]`.
1553        Face::Up | Face::Down => (0, 2),
1554    }
1555}
1556
1557/// A footprint's inclusive span on one WORLD axis (0 = x, 2 = z). Axis 1 has no
1558/// answer here — a footprint is horizontal — and no caller asks for it.
1559fn span(foot: [i64; 4], axis: usize) -> (i64, i64) {
1560    if axis == 0 {
1561        (foot[0], foot[1])
1562    } else {
1563        (foot[2], foot[3])
1564    }
1565}
1566
1567/// Inclusive overlap of two ranges, or `None`.
1568fn overlap(a: (i64, i64), b: (i64, i64)) -> Option<(i64, i64)> {
1569    let lo = a.0.max(b.0);
1570    let hi = a.1.min(b.1);
1571    (lo <= hi).then_some((lo, hi))
1572}
1573
1574/// Is `[lo, hi]` inside `[within_lo, within_hi]`?
1575fn within(r: (i64, i64), w: (i64, i64)) -> bool {
1576    r.0 >= w.0 && r.1 <= w.1
1577}
1578
1579/// The region's inclusive span on one axis.
1580fn region_span(region: &WorldBox, axis: usize) -> (i64, i64) {
1581    (region.min[axis], region.max()[axis])
1582}
1583
1584// ---------------------------------------------------------------------------
1585// The binding ledger's site-plan half
1586// ---------------------------------------------------------------------------
1587
1588/// What a run's site-plan checks bound to.
1589///
1590/// Carried inside [`crate::LayoutBinding`] rather than as a second ledger: the
1591/// map pipeline's documents are counted by one struct with one constructor, so
1592/// the number the CLI prints and the number a diagnostic quotes cannot disagree
1593/// about how many places there are.
1594#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
1595pub struct PlanBinding {
1596    /// Places embedded — what `DW0825`, `DW0826` and `DW0832` examine.
1597    pub boxes: usize,
1598    /// Unordered box pairs — what `DW0827` examines. Zero at one box, which is
1599    /// the honest count and not a pass.
1600    pub box_pairs: usize,
1601    /// Connections allocated — what `DW0828` and `DW0829` examine.
1602    pub seams: usize,
1603    /// Of those, seams whose edge is a `stair` — what `DW0830` examines.
1604    pub stair_seams: usize,
1605    /// Of those, seams whose edge is a `drop` — what `DW0831` examines.
1606    pub drop_seams: usize,
1607    /// Named ground planes.
1608    pub datums: usize,
1609    /// Whole-owned masses — what `DW0835` examines.
1610    pub volumes: usize,
1611    /// Guarded comparisons against the brief — what `DW0833` examines.
1612    pub identities: usize,
1613    /// Sightlines embedded.
1614    pub sightlines: usize,
1615    /// Named exterior vantages the walk judges the silhouette from.
1616    pub views: usize,
1617    /// Boxes whose corner the author pinned (spec-0059 §3).
1618    pub pinned: usize,
1619    /// Boxes whose corner a seam derived.
1620    pub derived: usize,
1621    /// Connected components of the seam graph.
1622    pub components: usize,
1623}
1624
1625impl PlanBinding {
1626    /// Count what a campaign's site plan offers the checks.
1627    #[must_use]
1628    pub fn of(c: &Campaign) -> PlanBinding {
1629        let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
1630            return PlanBinding::default();
1631        };
1632        let classes: BTreeMap<&str, &'static str> = c
1633            .layout_graph
1634            .as_ref()
1635            .map(|g| {
1636                g.content
1637                    .edges
1638                    .iter()
1639                    .map(|e| (e.id().0.as_str(), e.class()))
1640                    .collect()
1641            })
1642            .unwrap_or_default();
1643        let n = plan.boxes.len();
1644        let (pinned, derived, components) = match c.layout_graph.as_ref() {
1645            Some(g) => {
1646                let table = Metrics::table();
1647                let mut reads = Reads::new();
1648                let mut sink = Vec::new();
1649                let (_, packed) = resolve(plan, &g.content, &table, &mut reads, &mut sink);
1650                (packed.pinned, packed.derived, packed.components)
1651            }
1652            None => (0, 0, 0),
1653        };
1654        PlanBinding {
1655            pinned,
1656            derived,
1657            components,
1658            boxes: n,
1659            box_pairs: n * n.saturating_sub(1) / 2,
1660            seams: plan.seams.len(),
1661            stair_seams: plan
1662                .seams
1663                .iter()
1664                .filter(|s| classes.get(s.edge.0.as_str()) == Some(&"stair"))
1665                .count(),
1666            drop_seams: plan
1667                .seams
1668                .iter()
1669                .filter(|s| classes.get(s.edge.0.as_str()) == Some(&"drop"))
1670                .count(),
1671            datums: plan.datums.len(),
1672            volumes: plan.volumes.len(),
1673            identities: plan.identities.len(),
1674            sightlines: plan.sightlines.len(),
1675            views: plan.views.len(),
1676        }
1677    }
1678
1679    /// The site-plan half of the binding line.
1680    #[must_use]
1681    pub fn line(&self) -> String {
1682        format!(
1683            "site-plan binding: {b} box(es) ({p} pair(s) compared; {pn} pinned, {dv} derived, \
1684             in {cc} component(s)), {s} seam(s) ({st} stair, {sd} drop), {d} datum(s), {v} \
1685             whole-owned volume(s), {i} identity(ies), {sl} sightline(s), {w} view(s).",
1686            pn = self.pinned,
1687            dv = self.derived,
1688            cc = self.components,
1689            b = self.boxes,
1690            p = self.box_pairs,
1691            s = self.seams,
1692            st = self.stair_seams,
1693            sd = self.drop_seams,
1694            d = self.datums,
1695            v = self.volumes,
1696            i = self.identities,
1697            sl = self.sightlines,
1698            w = self.views,
1699        )
1700    }
1701}
1702
1703// ---------------------------------------------------------------------------
1704// Validation (spec-0049 §4.3) — every rule, all upstream of any geometry
1705// ---------------------------------------------------------------------------
1706
1707/// Every check the site plan owes, at **validation** tier (exit 1).
1708///
1709/// # What invokes this, and what happens without it
1710///
1711/// [`crate::validate::validate_campaign_with`], whenever the campaign directory
1712/// holds a `site-plan.json` — the same event-bound shape stages 2, 3 and 7 use.
1713/// That function is what **every** `delvec` subcommand's validation stage calls,
1714/// so there is no path from a campaign directory to a verdict, a world or a
1715/// datapack that goes round it: `validate`, `analyze` and `build` all enter
1716/// through it, and a caller who somehow skipped it would have no `Campaign` to
1717/// hand anything downstream. There is no flag to pass, no step in a document to
1718/// remember, and no second entry point.
1719///
1720/// # Why every rule is here and none is at analysis tier
1721///
1722/// Round 2 put the layout graph's *reachability* proofs at analysis tier because
1723/// reachability is a question about a whole graph, the tier its quest-graph
1724/// siblings answer at. Nothing here is that question. Every rule below is a
1725/// property of the document in front of it — does this box fit, do these two
1726/// boxes touch, does this number match the brief's — so all of it is validation,
1727/// and a plan that is wrong is wrong before anything is analyzed.
1728///
1729/// # What is deliberately NOT here, and where it went
1730///
1731/// Three obligations of this stage are only decidable once the blockout exists,
1732/// and each is named rather than approximated:
1733///
1734/// * whether a built seam is the opening the plan allocated, whether every node's
1735///   floor is reached, and whether any crossing was *discovered* outside a seam;
1736/// * `DW0833`'s second call site, the identities recomputed from assembled bytes;
1737/// * whether a declared sightline is unobstructed.
1738///
1739/// All three read blocks. Writing a version of them here that read the plan
1740/// instead would be the derivation's arithmetic replayed against itself — the
1741/// opposite of an independent observer — so they belong to the round that builds
1742/// the blockout, and this module states the plan-side half they will be checked
1743/// against.
1744pub fn check(c: &Campaign, reads: &mut Reads, d: &mut Vec<Diagnostic>) {
1745    let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
1746        return;
1747    };
1748    let table = Metrics::table();
1749
1750    // Plan-internal wellformedness first: the ids every rule below quotes.
1751    ids(plan, d);
1752    one_authority(c, d);
1753
1754    // ------------------------------------------------------------- the tooth
1755    // A site plan validates ONLY against a layout graph and a geometry brief.
1756    // Both are refused by name, and the graph's absence returns: every rule
1757    // below reads node ids, and without a graph each would be answering about a
1758    // place nothing declared.
1759    let brief_missing = c.geometry_brief.is_none();
1760    if brief_missing {
1761        d.push(Diagnostic::error(
1762            DW_PLAN_AGREEMENT,
1763            "site-plan",
1764            "",
1765            "this campaign carries a site plan and no `geometry-brief.json`. The plan is the \
1766             embedding of a design, and the brief is where that design's numbers are written \
1767             down; with no brief there is nothing for `identities[]` to hold the map to, and \
1768             the region's extent is a number with no author. Write the brief first — a plan \
1769             cannot reach green ahead of it."
1770                .to_string(),
1771        ));
1772    }
1773    let Some(graph) = c.layout_graph.as_ref().map(|g| &g.content) else {
1774        d.push(Diagnostic::error(
1775            DW_PLAN_AGREEMENT,
1776            "site-plan",
1777            "",
1778            "this campaign carries a site plan and no `layout-graph.json`. A site plan is the \
1779             geometric embedding OF a layout graph: every box names a place and every seam \
1780             names a connection, so with no graph there is nothing being embedded and every \
1781             name in this document resolves to nothing. This is the only line this state \
1782             raises: the map's anchor vocabulary is derived from the graph too, so every \
1783             anchor a `npcs`, `quests` or effect document names is left unjudged here rather \
1784             than refused against an empty set, and is judged the moment the graph exists. \
1785             Author the graph first — that ordering \
1786             is what this refusal exists to make uncompilable rather than merely advised."
1787                .to_string(),
1788        ));
1789        return;
1790    };
1791
1792    openers(c, graph, d);
1793    let (placed, packed) = resolve(plan, graph, &table, reads, d);
1794    agreement(plan, graph, &placed, d);
1795    grid(&placed, &table, reads, d);
1796    region(plan, &placed, d);
1797    disjoint(&placed, d);
1798    seams(plan, graph, &placed, &packed, &table, reads, d);
1799    size_classes(&placed, d);
1800    volumes_outside_boxes(plan, &placed, d);
1801    identities(c, plan, &placed, d);
1802    lighting(plan, d);
1803}
1804
1805/// `DW0839`: a campaign has ONE placement authority.
1806///
1807/// `areas[]` places pieces on the fixed stride; the site plan places the whole
1808/// map in its own region. A world carrying both has two owners for one question
1809/// and no rule to pick between them, so the answer is not to arbitrate but to
1810/// refuse. Both surfaces stay legal at 0.14.0 — one per campaign.
1811fn one_authority(c: &Campaign, d: &mut Vec<Diagnostic>) {
1812    let n = c.world.content.areas.len();
1813    if n == 0 {
1814        return;
1815    }
1816    d.push(Diagnostic::error(
1817        DW_TWO_AUTHORITIES,
1818        "world",
1819        "/content/areas",
1820        format!(
1821            "this campaign declares {n} `areas[]` entry(ies) AND a site plan. Those are two \
1822             placement authorities for one world: `areas[]` seats prefab pieces on the compiler's \
1823             fixed stride, and the site plan seats the derived blockout inside its own declared \
1824             `region` — so every question about where something is has two answers and nothing \
1825             says which. Keep one. A campaign that places pieces keeps `areas[]` and drops \
1826             `site-plan.json`; a campaign whose map is the site plan declares an empty `areas` \
1827             list and lets the plan own the space. Both surfaces are legal — what is not legal \
1828             is one \
1829             campaign holding both."
1830        ),
1831    ));
1832}
1833
1834/// `DW0818`'s **byte-side half** of the opener obligation, which round 3 could
1835/// not write and named as this round's.
1836///
1837/// The graph half already stands in [`crate::layout`]: a `barred` edge must
1838/// declare a `gating` that names a flag some effect really sets or a quest that
1839/// really exists. That says the way is *meant* to open; it does not say anything
1840/// in the campaign ever opens **this** way, because at stage 3 the region such
1841/// an effect would target does not exist yet. It exists here: the derivation
1842/// synthesizes [`seam_anchor`] over every `barred` seam's opening, so "something
1843/// opens `seam/<edge>`" is finally a question with a subject.
1844///
1845/// Raised under `DW0818` and against the layout graph, because the fault is the
1846/// graph's claim rather than the plan's geometry — the plan did everything asked
1847/// of it. Only reachable in a site-plan campaign, which is exactly the campaign
1848/// in which the seam anchor exists to be named.
1849fn openers(c: &Campaign, graph: &LayoutGraphContent, d: &mut Vec<Diagnostic>) {
1850    // Every gate region the campaign opens, however it opens it: an `open-gate`
1851    // at any nesting depth, or a `shortcut` whose far side lifts the bar.
1852    let mut opened: BTreeSet<&str> = BTreeSet::new();
1853    crate::stages::for_each_campaign_effect(c, &mut |_, _, eff| {
1854        eff.visit_deep(&mut |e| {
1855            if let Some(a) = e.open_gate_anchor() {
1856                opened.insert(a.0.as_str());
1857            }
1858        });
1859    });
1860    for s in &c.quests.content.shortcuts {
1861        opened.insert(s.gate.0.as_str());
1862    }
1863
1864    for (i, e) in graph.edges.iter().enumerate() {
1865        let Edge::Barred { id, .. } = e else { continue };
1866        let want = seam_anchor(id);
1867        if opened.contains(want.as_str()) {
1868            continue;
1869        }
1870        d.push(Diagnostic::error(
1871            crate::layout::DW_GRAPH_MISSION,
1872            "layout-graph",
1873            format!("/content/edges/{i}"),
1874            format!(
1875                "`{id}` is barred and nothing in this campaign opens it. The derivation seals \
1876                 this seam's opening at world load and names the region `{want}`; for the way to \
1877                 ever be passable some effect has to address that name — an `open-gate` on the \
1878                 beat whose completion earns it, or a `shortcut` whose far side lifts the bar. \
1879                 The graph's `gating` says what a body must HOLD to pass, which is a different \
1880                 claim and is already checked: a way that is gated on a flag nobody spends is \
1881                 still a wall. This is the half of the obligation that could only be written once \
1882                 the region existed, so it is asked here rather than at stage 3."
1883            ),
1884        ));
1885    }
1886}
1887
1888/// `datum/`, `volume/` and `view/` ids: well formed and unique, like every other
1889/// id in the DSL. An id is an id, so these are the ordinary `DW0110`/`DW0111`.
1890fn ids(plan: &SitePlanContent, d: &mut Vec<Diagnostic>) {
1891    let mut seen: BTreeSet<String> = BTreeSet::new();
1892    let mut check_id = |ok: bool, id: String, kind: &str, path: String, d: &mut Vec<Diagnostic>| {
1893        if !ok {
1894            d.push(Diagnostic::error(
1895                crate::codes::ID_SYNTAX,
1896                "site-plan",
1897                path.clone(),
1898                format!("malformed {kind} id `{id}` — expected `{kind}/<kebab-case>`."),
1899            ));
1900        }
1901        if !seen.insert(format!("{kind}:{id}")) {
1902            d.push(Diagnostic::error(
1903                crate::codes::ID_DUPLICATE,
1904                "site-plan",
1905                path,
1906                format!(
1907                    "duplicate {kind} id `{id}` — rename one, because anything naming it would \
1908                     otherwise name both."
1909                ),
1910            ));
1911        }
1912    };
1913    for (i, dat) in plan.datums.iter().enumerate() {
1914        check_id(
1915            dat.id.is_valid_syntax(),
1916            dat.id.0.clone(),
1917            "datum",
1918            format!("/content/datums/{i}/id"),
1919            d,
1920        );
1921    }
1922    for (i, v) in plan.volumes.iter().enumerate() {
1923        check_id(
1924            v.id.is_valid_syntax(),
1925            v.id.0.clone(),
1926            "volume",
1927            format!("/content/volumes/{i}/id"),
1928            d,
1929        );
1930    }
1931    for (i, v) in plan.views.iter().enumerate() {
1932        check_id(
1933            v.id.is_valid_syntax(),
1934            v.id.0.clone(),
1935            "view",
1936            format!("/content/views/{i}/id"),
1937            d,
1938        );
1939    }
1940}
1941
1942// ---------------------------------------------------------------------------
1943// Packing — a box is placed by its seam, and the grid is derived (spec-0059 §3)
1944// ---------------------------------------------------------------------------
1945
1946/// How a box came to stand where it stands.
1947#[derive(Debug, Clone, PartialEq, Eq)]
1948pub enum Provenance {
1949    /// The author pinned its `min`.
1950    Pinned,
1951    /// A seam hung it off a box that already stood.
1952    Seam {
1953        /// Index into `seams[]`.
1954        seam: usize,
1955        /// The connection the seam allocates.
1956        edge: EdgeId,
1957        /// The box it was hung off.
1958        from: NodeId,
1959        /// The face of `from` it hangs off — or, when `from` is the seam's `b`
1960        /// end, the face of this box the seam names.
1961        face: Face,
1962    },
1963}
1964
1965impl Provenance {
1966    /// The words a refusal or the placing line uses.
1967    fn describe(&self) -> String {
1968        match self {
1969            Provenance::Pinned => "pinned".to_string(),
1970            Provenance::Seam {
1971                edge, from, face, ..
1972            } => format!(
1973                "hung off `{from}` across the {face} face by the seam for `{edge}`",
1974                face = face.as_str()
1975            ),
1976        }
1977    }
1978}
1979
1980/// One box's corner, with its provenance.
1981#[derive(Debug, Clone, PartialEq, Eq)]
1982pub struct PackedBox {
1983    /// The place.
1984    pub node: NodeId,
1985    /// Low corner `[x, z]`, in world coordinates.
1986    pub min: [i64; 2],
1987    /// Where the corner came from.
1988    pub by: Provenance,
1989}
1990
1991/// The packing of one plan: every corner the pins and seams settle, and the
1992/// world anchor of every seam whose two ends stand.
1993#[derive(Debug, Default)]
1994struct Packed {
1995    /// Per `plan.boxes` index; `None` when nothing placed the box.
1996    boxes: Vec<Option<PackedBox>>,
1997    /// Per `plan.seams` index: the crossing's low corner on the face's own two
1998    /// world axes — `[along, sill]` on a wall, `[x, z]` through a floor or
1999    /// ceiling. `None` when either end is unplaced or a floor is unresolved.
2000    seam_at: Vec<Option<[i64; 2]>>,
2001    /// Seams the packing itself refused; [`seams`] does not judge them twice.
2002    refused: BTreeSet<usize>,
2003    /// Connected components of the seam graph, over boxes the graph declares.
2004    components: usize,
2005    pinned: usize,
2006    derived: usize,
2007}
2008
2009/// A seam resolved far enough to place a box: its two box indices and its two
2010/// offsets, each `[u, v]` on a horizontal face and `[u, 0]` on a vertical one.
2011#[derive(Debug, Clone, Copy)]
2012struct Link {
2013    a: usize,
2014    b: usize,
2015    at: [i64; 2],
2016    meets: [i64; 2],
2017}
2018
2019/// The two horizontal-axis indices a face's arithmetic uses: `(normal, along)`
2020/// into `[x, z]`.
2021fn face_axes(face: Face) -> (usize, usize) {
2022    match face {
2023        Face::East | Face::West => (0, 1),
2024        Face::North | Face::South => (1, 0),
2025        Face::Up | Face::Down => (0, 1), // unused: a horizontal face has no normal in [x, z]
2026    }
2027}
2028
2029/// The width a crossing is centred by, on the face's own two axes, or `None`
2030/// for a contact with no extent — the whole face, so the offsets default to 0.
2031/// `Err` when the opening cannot be resolved (`DW0812`/`DW0876` name that).
2032fn centring_width(s: &Seam, table: &Metrics, reads: &mut Reads) -> Result<Option<[i64; 2]>, ()> {
2033    if let Some(c) = &s.contact {
2034        return Ok(c
2035            .extent
2036            .map(|e| [i64::from(e[0].get()), i64::from(e[1].get())]));
2037    }
2038    let Some(name) = s.opening.as_deref() else {
2039        return Err(());
2040    };
2041    let entry = table.resolve(MetricKind::Opening, name).map_err(|_| ())?;
2042    match entry.value(reads) {
2043        MetricValue::Opening(o) => Ok(Some([i64::from(o.width), i64::from(o.height)])),
2044        _ => Err(()),
2045    }
2046}
2047
2048/// Why an offset is not a position on its face.
2049enum OffsetProblem {
2050    /// The shape does not match the face: an integer through a floor, a pair
2051    /// on a wall.
2052    Shape { which: &'static str },
2053    /// The crossing, anchored there, leaves the box's own face.
2054    OffFace {
2055        which: &'static str,
2056        axis: &'static str,
2057        value: i64,
2058        extent: i64,
2059        width: i64,
2060    },
2061}
2062
2063/// Resolve `at` and `meets` for one seam against its two boxes' extents:
2064/// declared values taken as written, omitted ones centred (spec-0059 §3).
2065fn offsets(
2066    s: &Seam,
2067    ext_a: [i64; 2],
2068    ext_b: [i64; 2],
2069    width: Option<[i64; 2]>,
2070) -> Result<([i64; 2], [i64; 2]), OffsetProblem> {
2071    let horizontal = s.face.is_horizontal_plane();
2072    let one = |which: &'static str,
2073               declared: Option<Offset>,
2074               ext: [i64; 2]|
2075     -> Result<[i64; 2], OffsetProblem> {
2076        // Which of `[dx, dz]` each offset component runs along, and its name.
2077        let (axes, names): ([usize; 2], [&'static str; 2]) = if horizontal {
2078            ([0, 1], ["x", "z"])
2079        } else {
2080            let (_, along) = face_axes(s.face);
2081            ([along, 0], [if along == 0 { "x" } else { "z" }, ""])
2082        };
2083        // Centred; a crossing wider than the face centres at the corner and
2084        // whether it fits is `DW0829`'s or `DW0876`'s question.
2085        let default = |i: usize| match width {
2086            Some(w) => (ext[axes[i]] - w[i]).div_euclid(2).max(0),
2087            None => 0,
2088        };
2089        let off = match (declared, horizontal) {
2090            (None, true) => [default(0), default(1)],
2091            (None, false) => [default(0), 0],
2092            (Some(Offset::Plane(p)), true) => p,
2093            (Some(Offset::Along(u)), false) => [u, 0],
2094            _ => return Err(OffsetProblem::Shape { which }),
2095        };
2096        let n = if horizontal { 2 } else { 1 };
2097        for i in 0..n {
2098            // The CORNER is on the face; whether the crossing fits from there is
2099            // `DW0829`'s (a portal) or `DW0876`'s (a contact) question, as ever.
2100            let extent = ext[axes[i]];
2101            let w = width.map_or(1, |w| w[i]);
2102            if off[i] < 0 || off[i] > extent - 1 {
2103                return Err(OffsetProblem::OffFace {
2104                    which,
2105                    axis: names[i],
2106                    value: off[i],
2107                    extent,
2108                    width: w,
2109                });
2110            }
2111        }
2112        Ok(off)
2113    };
2114    Ok((one("at", s.at, ext_a)?, one("meets", s.meets, ext_b)?))
2115}
2116
2117/// The corner of the box a seam places, from the corner of the one that
2118/// already stands. `from_a` places `b` off `a`; otherwise `a` off `b`.
2119fn derive_corner(
2120    face: Face,
2121    known: [i64; 2],
2122    ext_a: [i64; 2],
2123    ext_b: [i64; 2],
2124    at: [i64; 2],
2125    meets: [i64; 2],
2126    from_a: bool,
2127) -> [i64; 2] {
2128    if face.is_horizontal_plane() {
2129        return if from_a {
2130            [known[0] + at[0] - meets[0], known[1] + at[1] - meets[1]]
2131        } else {
2132            [known[0] - at[0] + meets[0], known[1] - at[1] + meets[1]]
2133        };
2134    }
2135    let (normal, along) = face_axes(face);
2136    let positive = matches!(face, Face::East | Face::South);
2137    let mut out = [0i64; 2];
2138    if from_a {
2139        out[along] = known[along] + at[0] - meets[0];
2140        out[normal] = if positive {
2141            known[normal] + ext_a[normal] + 1
2142        } else {
2143            known[normal] - ext_b[normal] - 1
2144        };
2145    } else {
2146        out[along] = known[along] - at[0] + meets[0];
2147        out[normal] = if positive {
2148            known[normal] - ext_a[normal] - 1
2149        } else {
2150            known[normal] + ext_b[normal] + 1
2151        };
2152    }
2153    out
2154}
2155
2156/// The crossing's low corner in the face's own two world axes — what every
2157/// seam rule judges. `[along, sill]` on a wall, the sill being the higher of
2158/// the two floors; `[x, z]` through a floor or ceiling.
2159fn crossing_anchor(
2160    face: Face,
2161    a_min: [i64; 2],
2162    at: [i64; 2],
2163    floor_a: i64,
2164    floor_b: i64,
2165) -> [i64; 2] {
2166    if face.is_horizontal_plane() {
2167        [a_min[0] + at[0], a_min[1] + at[1]]
2168    } else {
2169        let (_, along) = face_axes(face);
2170        [a_min[along] + at[0], floor_a.max(floor_b)]
2171    }
2172}
2173
2174fn ext_of(b: &PlanBox) -> [i64; 2] {
2175    [i64::from(b.extent[0].get()), i64::from(b.extent[1].get())]
2176}
2177
2178/// **The packing** (spec-0059 §3): the pinned boxes seed it; then `seams[]` in
2179/// document order, repeatedly, each seam with exactly one end standing placing
2180/// the other, until a pass places nothing. A seam whose two ends both stand is
2181/// then checked — the corner it would derive against the corner the box has —
2182/// and a component of the seam graph with no pinned box is refused, because
2183/// nothing places it.
2184fn pack(
2185    plan: &SitePlanContent,
2186    graph: &LayoutGraphContent,
2187    table: &Metrics,
2188    floors: &[Option<i64>],
2189    reads: &mut Reads,
2190    d: &mut Vec<Diagnostic>,
2191) -> Packed {
2192    let mut out = Packed {
2193        boxes: vec![None; plan.boxes.len()],
2194        seam_at: vec![None; plan.seams.len()],
2195        ..Packed::default()
2196    };
2197    let nodes: BTreeSet<&str> = graph.nodes.iter().map(|n| n.id.0.as_str()).collect();
2198    let mut by_node: BTreeMap<&str, usize> = BTreeMap::new();
2199    for (i, b) in plan.boxes.iter().enumerate() {
2200        if nodes.contains(b.node.0.as_str()) {
2201            by_node.entry(b.node.0.as_str()).or_insert(i);
2202        }
2203    }
2204    let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
2205
2206    // ---- seeds
2207    for (i, b) in plan.boxes.iter().enumerate() {
2208        if let Some(min) = b.min {
2209            out.boxes[i] = Some(PackedBox {
2210                node: b.node.clone(),
2211                min,
2212                by: Provenance::Pinned,
2213            });
2214            out.pinned += 1;
2215        }
2216    }
2217
2218    // ---- the links: every seam whose edge and boxes resolve, with its offsets
2219    let mut pairs: Vec<(usize, usize)> = Vec::new();
2220    let mut links: Vec<Option<Link>> = Vec::with_capacity(plan.seams.len());
2221    for (i, s) in plan.seams.iter().enumerate() {
2222        let Some(edge) = edges.get(s.edge.0.as_str()) else {
2223            links.push(None);
2224            continue; // `DW0824` refused the reference.
2225        };
2226        if !edge.has_seam() {
2227            links.push(None);
2228            continue; // `DW0824` said this carries a sightline.
2229        }
2230        let (Some(&a), Some(&b)) = (
2231            by_node.get(edge.a().0.as_str()),
2232            by_node.get(edge.b().0.as_str()),
2233        ) else {
2234            links.push(None);
2235            continue; // `DW0824` reported the missing box.
2236        };
2237        pairs.push((a, b));
2238        // An unresolvable opening (`DW0812`/`DW0876` say so) centres nothing:
2239        // the crossing is taken one cell wide at the corner, so the seam still
2240        // places and the rules that name the opening run over a plan that
2241        // stands.
2242        let width = centring_width(s, table, reads).unwrap_or_default();
2243        match offsets(s, ext_of(&plan.boxes[a]), ext_of(&plan.boxes[b]), width) {
2244            Ok((at, meets)) => links.push(Some(Link { a, b, at, meets })),
2245            Err(problem) => {
2246                d.push(offset_problem(i, s, edge, problem));
2247                out.refused.insert(i);
2248                links.push(None);
2249            }
2250        }
2251    }
2252
2253    // ---- placement passes
2254    loop {
2255        let mut changed = false;
2256        for (i, link) in links.iter().enumerate() {
2257            let Some(l) = link else { continue };
2258            let s = &plan.seams[i];
2259            let (ext_a, ext_b) = (ext_of(&plan.boxes[l.a]), ext_of(&plan.boxes[l.b]));
2260            match (out.boxes[l.a].clone(), out.boxes[l.b].clone()) {
2261                (Some(a), None) => {
2262                    let min = derive_corner(s.face, a.min, ext_a, ext_b, l.at, l.meets, true);
2263                    out.boxes[l.b] = Some(PackedBox {
2264                        node: plan.boxes[l.b].node.clone(),
2265                        min,
2266                        by: Provenance::Seam {
2267                            seam: i,
2268                            edge: s.edge.clone(),
2269                            from: a.node,
2270                            face: s.face,
2271                        },
2272                    });
2273                    out.derived += 1;
2274                    changed = true;
2275                }
2276                (None, Some(b)) => {
2277                    let min = derive_corner(s.face, b.min, ext_a, ext_b, l.at, l.meets, false);
2278                    out.boxes[l.a] = Some(PackedBox {
2279                        node: plan.boxes[l.a].node.clone(),
2280                        min,
2281                        by: Provenance::Seam {
2282                            seam: i,
2283                            edge: s.edge.clone(),
2284                            from: b.node,
2285                            face: s.face,
2286                        },
2287                    });
2288                    out.derived += 1;
2289                    changed = true;
2290                }
2291                _ => {}
2292            }
2293        }
2294        if !changed {
2295            break;
2296        }
2297    }
2298
2299    // ---- every seam whose two ends stand: one placement, and the anchor
2300    for (i, link) in links.iter().enumerate() {
2301        let Some(l) = link else { continue };
2302        let (Some(a), Some(b)) = (&out.boxes[l.a], &out.boxes[l.b]) else {
2303            continue;
2304        };
2305        let s = &plan.seams[i];
2306        let (ext_a, ext_b) = (ext_of(&plan.boxes[l.a]), ext_of(&plan.boxes[l.b]));
2307        let want = derive_corner(s.face, a.min, ext_a, ext_b, l.at, l.meets, true);
2308        if want != b.min {
2309            d.push(two_placements(i, s, a, b, want));
2310            out.refused.insert(i);
2311            continue;
2312        }
2313        if let (Some(fa), Some(fb)) = (floors[l.a], floors[l.b]) {
2314            out.seam_at[i] = Some(crossing_anchor(s.face, a.min, l.at, fa, fb));
2315        }
2316    }
2317
2318    // ---- components with nothing to place them
2319    let mut parent: Vec<usize> = (0..plan.boxes.len()).collect();
2320    fn find(p: &mut [usize], i: usize) -> usize {
2321        let mut r = i;
2322        while p[r] != r {
2323            r = p[r];
2324        }
2325        let mut c = i;
2326        while p[c] != r {
2327            let n = p[c];
2328            p[c] = r;
2329            c = n;
2330        }
2331        r
2332    }
2333    for &(a, b) in &pairs {
2334        let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
2335        if ra != rb {
2336            parent[ra.max(rb)] = ra.min(rb);
2337        }
2338    }
2339    let mut members: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
2340    for (i, b) in plan.boxes.iter().enumerate() {
2341        if nodes.contains(b.node.0.as_str()) {
2342            let r = find(&mut parent, i);
2343            members.entry(r).or_default().push(i);
2344        }
2345    }
2346    out.components = members.len();
2347    let entry = graph.entry.0.as_str();
2348    for (_, boxes) in members {
2349        if boxes.iter().any(|&i| plan.boxes[i].min.is_some()) {
2350            continue;
2351        }
2352        let names: Vec<String> = boxes
2353            .iter()
2354            .map(|&i| format!("`{}`", plan.boxes[i].node))
2355            .collect();
2356        let suggested = boxes
2357            .iter()
2358            .find(|&&i| plan.boxes[i].node.0 == entry)
2359            .or(boxes.first())
2360            .map(|&i| plan.boxes[i].node.to_string())
2361            .unwrap_or_default();
2362        d.push(Diagnostic::error(
2363            DW_UNPLACED,
2364            "site-plan",
2365            format!("/content/boxes/{}", boxes[0]),
2366            format!(
2367                "nothing places {list}: no box among them pins its `min`, and a box stands \
2368                 only where a pin puts it or where a seam hangs it off a box that already \
2369                 stands. Pin one of them — `{suggested}` — with `\"min\": [x, z]`, and the \
2370                 seams place the rest. {count} box(es) in this component.",
2371                list = names.join(", "),
2372                count = boxes.len(),
2373            ),
2374        ));
2375    }
2376    out
2377}
2378
2379/// `DW0828`: an offset that is not a position on its own box's face.
2380fn offset_problem(i: usize, s: &Seam, edge: &Edge, problem: OffsetProblem) -> Diagnostic {
2381    let (which, detail) = match problem {
2382        OffsetProblem::Shape { which } => (
2383            which,
2384            if s.face.is_horizontal_plane() {
2385                format!(
2386                    "the {face} face is a floor or ceiling with two in-plane axes, and `{which}` \
2387                     gives one number. Write `[dx, dz]` — cells along x and z from the box's \
2388                     low corner",
2389                    face = s.face.as_str()
2390                )
2391            } else {
2392                format!(
2393                    "the {face} face is a wall with one horizontal axis, and `{which}` gives \
2394                     two numbers. Write one — cells along the face from the box's low corner; \
2395                     the sill is not written, it is the higher of the two floors",
2396                    face = s.face.as_str()
2397                )
2398            },
2399        ),
2400        OffsetProblem::OffFace {
2401            which,
2402            axis,
2403            value,
2404            extent,
2405            width,
2406        } => (
2407            which,
2408            format!(
2409                "`{which}` puts the crossing's corner at {value} along {axis} on a face that \
2410                 runs 0..{last} — the crossing is {width} wide and the box is {extent} on that \
2411                 axis. Write an offset on the face, or omit it and the crossing is centred; an \
2412                 offset is never quietly clamped to fit",
2413                last = extent - 1,
2414            ),
2415        ),
2416    };
2417    Diagnostic::error(
2418        DW_SEAM_NOT_SHARED,
2419        "site-plan",
2420        format!("/content/seams/{i}/{which}"),
2421        format!(
2422            "the seam for `{id}` between `{an}` and `{bn}` names no position on the {side} \
2423             box's face: {detail}.",
2424            id = s.edge,
2425            an = edge.a(),
2426            bn = edge.b(),
2427            side = if which == "at" { "`a`" } else { "`b`" },
2428        ),
2429    )
2430}
2431
2432/// A seam whose two boxes both stand, and which would put `b` somewhere else:
2433/// `DW0883` when a pin is one of the two authorities, `DW0828` when a loop does
2434/// not close.
2435fn two_placements(i: usize, s: &Seam, a: &PackedBox, b: &PackedBox, want: [i64; 2]) -> Diagnostic {
2436    // Two authorities on `b`: its pin and this seam. When `b` was placed by
2437    // another seam, the disagreement is between seams — a loop that does not
2438    // close — however `a` came to stand where it stands.
2439    let pinned = matches!(b.by, Provenance::Pinned);
2440    let where_ = format!(
2441        "`{an}` stands at [{ax}, {az}] ({a_by}); `{bn}` stands at [{bx}, {bz}] ({b_by}); hung \
2442         off `{an}`'s {face} face by this seam, `{bn}` would stand at [{wx}, {wz}]",
2443        an = a.node,
2444        ax = a.min[0],
2445        az = a.min[1],
2446        a_by = a.by.describe(),
2447        bn = b.node,
2448        bx = b.min[0],
2449        bz = b.min[1],
2450        b_by = b.by.describe(),
2451        face = s.face.as_str(),
2452        wx = want[0],
2453        wz = want[1],
2454    );
2455    if pinned {
2456        Diagnostic::error(
2457            DW_UNPLACED,
2458            "site-plan",
2459            format!("/content/seams/{i}"),
2460            format!(
2461                "two things place one box, and they disagree: {where_}. A pin is a claim the \
2462                 packing verifies, never a second authority — move the pin to the corner the \
2463                 seam derives, delete it and let the seam place the box, or change this seam's \
2464                 `at`/`meets` so the two agree.",
2465            ),
2466        )
2467    } else {
2468        Diagnostic::error(
2469            DW_SEAM_NOT_SHARED,
2470            "site-plan",
2471            format!("/content/seams/{i}"),
2472            format!(
2473                "the seam for `{id}` closes a loop, and the loop does not close: {where_}. Every \
2474                 box in the loop was placed by an earlier seam, so this one can only check; \
2475                 change its `at`/`meets` to where the two boxes really meet, or move the \
2476                 offsets of the seams that placed them.",
2477                id = s.edge,
2478            ),
2479        )
2480    }
2481}
2482
2483/// Every box's corner and how it was obtained, one line each — the derivation
2484/// handed back, so a creator reads a corner from the build rather than typing
2485/// it into the document.
2486#[must_use]
2487pub fn placements(c: &Campaign) -> Vec<String> {
2488    let (Some(plan), Some(graph)) = (
2489        c.site_plan.as_ref().map(|p| &p.content),
2490        c.layout_graph.as_ref().map(|g| &g.content),
2491    ) else {
2492        return Vec::new();
2493    };
2494    let table = Metrics::table();
2495    let mut reads = Reads::new();
2496    let mut sink = Vec::new();
2497    let (_, packed) = resolve(plan, graph, &table, &mut reads, &mut sink);
2498    packed
2499        .boxes
2500        .iter()
2501        .flatten()
2502        .map(|b| {
2503            format!(
2504                "site-plan placing: `{node}` stands at [{x}, {z}] — {by}.",
2505                node = b.node,
2506                x = b.min[0],
2507                z = b.min[1],
2508                by = b.by.describe(),
2509            )
2510        })
2511        .collect()
2512}
2513
2514/// Resolve every box once: its footprint, its walk plane, its headroom and its
2515/// size class. A floor naming a datum the plan does not declare is the ordinary
2516/// dangling reference (`DW0112`) and the box is dropped, because a place with no
2517/// plane has no geometry for any rule below to judge.
2518/// The corners come from the packing (spec-0059 §3), which runs here so that
2519/// every reader of the resolved plan — the checks, the derivation, the battery
2520/// — holds one grid.
2521fn resolve<'a>(
2522    plan: &'a SitePlanContent,
2523    graph: &LayoutGraphContent,
2524    table: &Metrics,
2525    reads: &mut Reads,
2526    d: &mut Vec<Diagnostic>,
2527) -> (Vec<Placed<'a>>, Packed) {
2528    let datums: BTreeMap<&str, i64> = plan.datums.iter().map(|x| (x.id.0.as_str(), x.y)).collect();
2529    // Whichever of the two classifications the node declared. `DW0875` is what
2530    // refuses a node that declared both or neither; this map takes the size
2531    // class first so that a node which slipped past with both is judged against
2532    // one of the two rather than against neither — a refused campaign builds
2533    // nothing either way, and a check that quietly examines zero boxes is the
2534    // shape worth avoiding.
2535    let classes: BTreeMap<&str, (MetricKind, &str)> = graph
2536        .nodes
2537        .iter()
2538        .filter_map(|n| {
2539            let named = n
2540                .size_class
2541                .as_deref()
2542                .map(|x| (MetricKind::SizeClass, x))
2543                .or_else(|| n.way_class.as_deref().map(|x| (MetricKind::WayClass, x)))?;
2544            Some((n.id.0.as_str(), named))
2545        })
2546        .collect();
2547    // Floors first: the packing needs them for every sill, and a box with no
2548    // plane has no cells for any reader to work in.
2549    let mut floors: Vec<Option<i64>> = Vec::with_capacity(plan.boxes.len());
2550    for (i, b) in plan.boxes.iter().enumerate() {
2551        floors.push(match &b.floor {
2552            Floor::Y(y) => Some(*y),
2553            Floor::Datum(id) => match datums.get(id.0.as_str()) {
2554                Some(y) => Some(*y),
2555                None => {
2556                    d.push(Diagnostic::error(
2557                        crate::codes::DANGLING_REF,
2558                        "site-plan",
2559                        format!("/content/boxes/{i}/floor"),
2560                        format!(
2561                            "box for `{node}` stands on `{id}`, which this plan declares no \
2562                             `datums[]` entry for. Declare the plane, or give the box its own \
2563                             `y` — a place with no plane has no walk surface, so nothing below \
2564                             can say where it is.",
2565                            node = b.node,
2566                        ),
2567                    ));
2568                    None
2569                }
2570            },
2571        });
2572    }
2573    let packed = pack(plan, graph, table, &floors, reads, d);
2574    let mut out = Vec::new();
2575    for (i, b) in plan.boxes.iter().enumerate() {
2576        let (Some(floor), Some(pb)) = (floors[i], &packed.boxes[i]) else {
2577            continue; // `DW0112` or `DW0883` said why this box has no cells.
2578        };
2579        let class = classes
2580            .get(b.node.0.as_str())
2581            .and_then(|(kind, name)| table.resolve(*kind, name).ok())
2582            .and_then(|entry| match entry.value(reads) {
2583                MetricValue::SizeClass(sc) => Some(PlaceClass::Size(*sc)),
2584                MetricValue::WayClass(w) => Some(PlaceClass::Way(*w)),
2585                _ => None,
2586            });
2587        let clearance = match b.ceiling {
2588            Ceiling::Clearance(c) => Some(c.get()),
2589            // A sky-open place claims its class's own minimum headroom and
2590            // nothing above it: an open place is precisely one that makes no
2591            // claim on the air over it. True of both kinds of class, which is
2592            // why the question is asked of the classification rather than of
2593            // one of its variants.
2594            Ceiling::Open => class.map(PlaceClass::min_clearance),
2595        };
2596        out.push(Placed {
2597            index: i,
2598            plan: b,
2599            foot: [
2600                pb.min[0],
2601                pb.min[0] + i64::from(b.extent[0].get()) - 1,
2602                pb.min[1],
2603                pb.min[1] + i64::from(b.extent[1].get()) - 1,
2604            ],
2605            floor,
2606            clearance,
2607            class,
2608            by: pb.by.clone(),
2609        });
2610    }
2611    (out, packed)
2612}
2613
2614/// `DW0824`: the graph and the plan agree **exactly**, in both directions.
2615///
2616/// Six correspondences and three references, all one claim: everything the graph
2617/// declares is embedded exactly once, and everything the plan embeds is
2618/// something the graph declared.
2619///
2620/// This check is also the **two-artifact question's instrument** (spec-0049
2621/// §10): how often it fires *alone* — a graph edit with no plan edit or the
2622/// reverse — is the measurable evidence that decides whether the graph and the
2623/// plan stay two documents or merge into one.
2624///
2625/// It reads `plan.boxes` rather than the resolved list, deliberately: a box
2626/// whose floor named no datum is still a box the author wrote, and reporting it
2627/// as a place with no box as well would answer a question nobody asked.
2628fn agreement(
2629    plan: &SitePlanContent,
2630    graph: &LayoutGraphContent,
2631    placed: &[Placed<'_>],
2632    d: &mut Vec<Diagnostic>,
2633) {
2634    let fault = |path: String, msg: String, d: &mut Vec<Diagnostic>| {
2635        d.push(Diagnostic::error(DW_PLAN_AGREEMENT, "site-plan", path, msg));
2636    };
2637
2638    // -------------------------------------------------------------- places
2639    let nodes: BTreeSet<&str> = graph.nodes.iter().map(|n| n.id.0.as_str()).collect();
2640    let mut boxed: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
2641    for (i, b) in plan.boxes.iter().enumerate() {
2642        if !nodes.contains(b.node.0.as_str()) {
2643            fault(
2644                format!("/content/boxes/{i}/node"),
2645                format!(
2646                    "this box embeds `{n}`, which the layout graph declares no place for. A box \
2647                     is the geometry OF a place; one that names nothing is a room the map has \
2648                     no reason to contain. Declare the place, or delete the box.",
2649                    n = b.node,
2650                ),
2651                d,
2652            );
2653            continue;
2654        }
2655        boxed.entry(b.node.0.as_str()).or_default().push(i);
2656    }
2657    for (i, n) in graph.nodes.iter().enumerate() {
2658        match boxed.get(n.id.0.as_str()).map_or(0, Vec::len) {
2659            1 => {}
2660            0 => fault(
2661                "/content/boxes".to_string(),
2662                format!(
2663                    "place `{id}` has no box. Every place the graph declares is embedded exactly \
2664                     once — an unembedded place is a room the plan forgot, and nothing later can \
2665                     notice it, because every geometric rule quantifies over the boxes that \
2666                     exist. (Graph node {i} of {total}.)",
2667                    id = n.id,
2668                    total = graph.nodes.len(),
2669                ),
2670                d,
2671            ),
2672            k => fault(
2673                "/content/boxes".to_string(),
2674                format!(
2675                    "place `{id}` has {k} boxes. A place is one space; two boxes for it make \
2676                     every rule below pick one of them and no rule says which.",
2677                    id = n.id,
2678                ),
2679                d,
2680            ),
2681        }
2682    }
2683
2684    // --------------------------------------------------------- connections
2685    let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
2686    let mut seamed: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
2687    for (i, s) in plan.seams.iter().enumerate() {
2688        match edges.get(s.edge.0.as_str()) {
2689            None => fault(
2690                format!("/content/seams/{i}/edge"),
2691                format!(
2692                    "this seam allocates `{e}`, which the layout graph declares no connection \
2693                     for. A seam is an opening cut for a connection; one that names nothing is a \
2694                     hole in a wall for no reason.",
2695                    e = s.edge,
2696                ),
2697                d,
2698            ),
2699            Some(Edge::Vision { .. }) => fault(
2700                format!("/content/seams/{i}/edge"),
2701                format!(
2702                    "`{e}` is a `vision` connection and carries a **sightline**, not a seam. A \
2703                     seam is an opening on a shared face, and a vista's two ends are routinely \
2704                     not adjacent — a tower seen from a shore shares no face with it — so the \
2705                     seam construct cannot state the one thing a vision connection asserts. Move \
2706                     it to `sightlines[]`.",
2707                    e = s.edge,
2708                ),
2709                d,
2710            ),
2711            Some(Edge::Carry { .. }) => fault(
2712                format!("/content/seams/{i}/edge"),
2713                format!(
2714                    "`{e}` is a `carry` connection: a body crosses it by being put down on the \
2715                     far side by a link, so no opening is cut for it. Remove this seam.",
2716                    e = s.edge,
2717                ),
2718                d,
2719            ),
2720            Some(_) => {
2721                seamed.entry(s.edge.0.as_str()).or_default().push(i);
2722            }
2723        }
2724    }
2725    let mut sighted: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
2726    for (i, s) in plan.sightlines.iter().enumerate() {
2727        match edges.get(s.edge.0.as_str()) {
2728            None => fault(
2729                format!("/content/sightlines/{i}/edge"),
2730                format!(
2731                    "this sightline embeds `{e}`, which the layout graph declares no connection \
2732                     for.",
2733                    e = s.edge,
2734                ),
2735                d,
2736            ),
2737            Some(Edge::Carry { .. }) => fault(
2738                format!("/content/sightlines/{i}/edge"),
2739                format!(
2740                    "`{e}` is a `carry` connection: a body crosses it by being put down on the \
2741                     far side by a link, so nothing is embedded for it. Remove this sightline.",
2742                    e = s.edge,
2743                ),
2744                d,
2745            ),
2746            Some(e) if !matches!(e, Edge::Vision { .. }) => fault(
2747                format!("/content/sightlines/{i}/edge"),
2748                format!(
2749                    "`{id}` is a `{class}` connection: a body passes along it, so it is allocated \
2750                     a **seam** on a shared face rather than a line of sight. Move it to \
2751                     `seams[]`.",
2752                    id = s.edge,
2753                    class = e.class(),
2754                ),
2755                d,
2756            ),
2757            Some(_) => {
2758                sighted.entry(s.edge.0.as_str()).or_default().push(i);
2759            }
2760        }
2761    }
2762    for (i, e) in graph.edges.iter().enumerate() {
2763        // A carry is owed neither: a body crosses it by being put down on the
2764        // far side, and the geometry has nothing to allocate for that.
2765        if matches!(e, Edge::Carry { .. }) {
2766            continue;
2767        }
2768        let (what, held, other) = if e.is_traversal() {
2769            ("seam", &seamed, "seams")
2770        } else {
2771            ("sightline", &sighted, "sightlines")
2772        };
2773        match held.get(e.id().0.as_str()).map_or(0, Vec::len) {
2774            1 => {}
2775            0 => fault(
2776                format!("/content/{other}"),
2777                format!(
2778                    "connection `{id}` ({class}) has no {what}. **Seams are allocated, not \
2779                     discovered**: two places connect because the plan cut an opening between \
2780                     them while both were still free to move, never because a wall happened to \
2781                     be low somewhere. A connection with nothing allocated is a promise the \
2782                     geometry has not been asked to keep. (Graph edge {i} of {total}.)",
2783                    id = e.id(),
2784                    class = e.class(),
2785                    total = graph.edges.len(),
2786                ),
2787                d,
2788            ),
2789            k => fault(
2790                format!("/content/{other}"),
2791                format!(
2792                    "connection `{id}` has {k} {what}s. One connection is one way through; two \
2793                     openings for it are two ways, and the graph declared one.",
2794                    id = e.id(),
2795                ),
2796                d,
2797            ),
2798        }
2799    }
2800
2801    // ---------------------------------- what a seam says ABOUT its connection
2802    let by_node: BTreeMap<&str, &Placed<'_>> =
2803        placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
2804    for (i, s) in plan.seams.iter().enumerate() {
2805        let Some(e) = edges.get(s.edge.0.as_str()) else {
2806            continue;
2807        };
2808        let Some(host) = &s.stair_in else {
2809            continue;
2810        };
2811        if !matches!(e, Edge::Stair { .. }) {
2812            fault(
2813                format!("/content/seams/{i}/stair_in"),
2814                format!(
2815                    "this seam declares stair massing in `{host}`, and `{id}` is a `{class}` \
2816                     connection. Only a stair is built out of treads; on anything else the \
2817                     declaration is a fact about the geometry that the graph contradicts.",
2818                    id = s.edge,
2819                    class = e.class(),
2820                ),
2821                d,
2822            );
2823        } else if host != e.a() && host != e.b() {
2824            fault(
2825                format!("/content/seams/{i}/stair_in"),
2826                format!(
2827                    "this seam hosts its stair in `{host}`, which is neither end of `{id}` \
2828                     (`{a}` and `{b}`). A stair stands in one of the two places it joins.",
2829                    id = s.edge,
2830                    a = e.a(),
2831                    b = e.b(),
2832                ),
2833                d,
2834            );
2835        }
2836    }
2837
2838    // ------------------------- what a sightline says ABOUT its two places
2839    for (i, s) in plan.sightlines.iter().enumerate() {
2840        let Some(e) = edges.get(s.edge.0.as_str()) else {
2841            continue;
2842        };
2843        for (end, point, node) in [("from", s.from, e.a()), ("to", s.to, e.b())] {
2844            let Some(p) = by_node.get(node.0.as_str()) else {
2845                continue;
2846            };
2847            if contains_point(p, point) {
2848                continue;
2849            }
2850            fault(
2851                format!("/content/sightlines/{i}/{end}"),
2852                format!(
2853                    "`{id}` is a line of sight between `{a}` and `{b}`, and its `{end}` end \
2854                     `[{x}, {y}, {z}]` is not inside `{node}`. The stage-5 proof walks exactly \
2855                     this segment and calls the result the vista's; a segment whose ends are \
2856                     somewhere else would be proving a different claim, green or red.",
2857                    id = s.edge,
2858                    a = e.a(),
2859                    b = e.b(),
2860                    x = point[0],
2861                    y = point[1],
2862                    z = point[2],
2863                ),
2864                d,
2865            );
2866        }
2867    }
2868}
2869
2870/// Is a world cell inside a place's play space?
2871fn contains_point(p: &Placed<'_>, at: [i64; 3]) -> bool {
2872    if at[0] < p.x0() || at[0] > p.x1() || at[2] < p.z0() || at[2] > p.z1() {
2873        return false;
2874    }
2875    match p.y_span() {
2876        Some((lo, hi)) => at[1] >= lo && at[1] <= hi,
2877        // A sky-open place whose class did not resolve has no stated headroom;
2878        // `DW0812` already refused the name, and inventing a bound here would be
2879        // a second refusal for one defect.
2880        None => at[1] >= p.floor,
2881    }
2882}
2883
2884/// `DW0825`: every box's footprint is a multiple of the kit grid's quantum.
2885fn grid(placed: &[Placed<'_>], table: &Metrics, reads: &mut Reads, d: &mut Vec<Diagnostic>) {
2886    let Some(grid) = table.grid(reads) else {
2887        return; // `Metrics::self_check` owns a table that defines no grid.
2888    };
2889    let q = grid.quantum;
2890    if q == 0 {
2891        return;
2892    }
2893    for p in placed {
2894        for (axis, name) in [(0usize, "x"), (1usize, "z")] {
2895            let e = p.plan.extent[axis].get();
2896            if !off_grid(e, q) {
2897                continue;
2898            }
2899            d.push(Diagnostic::error(
2900                DW_BOX_OFF_GRID,
2901                "site-plan",
2902                format!("/content/boxes/{}/extent/{axis}", p.index),
2903                format!(
2904                    "box for `{node}` is {e} blocks on {name}, and the kit grid's quantum is \
2905                     {q} — so it is not a multiple of it. Every box's footprint is a whole \
2906                     number of quanta on both horizontal axes, which is what lets a kit piece \
2907                     land in one without being cut. The nearest multiples are {lo} and {hi}.",
2908                    node = p.plan.node,
2909                    lo = e - e % q,
2910                    hi = e - e % q + q,
2911                ),
2912            ));
2913        }
2914    }
2915}
2916
2917/// **`DW0825`'s own test, for one footprint on one axis.** One function so that
2918/// a verdict computed from a box can ask the SAME question the refusal asked,
2919/// rather than re-deriving the kit-grid rule beside it.
2920fn off_grid(extent: u32, quantum: u32) -> bool {
2921    quantum != 0 && !extent.is_multiple_of(quantum)
2922}
2923
2924/// **The clause a stage-6 verdict owes when the allocation it measured against
2925/// is one this plan has already refused** — the DEFER shape of
2926/// [`crate::diagnostic`]'s "one cause, one line".
2927///
2928/// A `details[]` row is judged against a FRAME and a SEAM SET, and both are
2929/// computed from the site plan. When the plan's own refusals have already
2930/// touched them, the stage-6 line is a true, separate finding measured against a
2931/// number the map does not really have — and, worse, the primary is in another
2932/// document, so the reader has no way to see the relation. Measured: widening
2933/// one box by one block printed `DW0825` and `DW0828` in the site plan and then
2934/// `DW0843` and `DW0844` in the detail plan, five codes over three documents,
2935/// with nothing saying which was the edit.
2936///
2937/// So the stage-6 verdicts keep their own lines — each still names a real
2938/// mismatch, and suppressing them is how fixing one thing produces a fresh crop
2939/// of refusals nobody was shown — and gain a clause saying what they are
2940/// downstream of. Two things can be already-refused:
2941///
2942/// 1. the place's own box is off the kit grid (`DW0825`), so its frame is not a
2943///    frame this map will keep;
2944/// 2. the plan DECLARES a seam on this place that it did not resolve — a face
2945///    the two boxes do not share (`DW0828`) or an opening that does not fit
2946///    (`DW0829`) — so the allocated seam set this place answers is short of what
2947///    the author wrote.
2948///
2949/// Empty when the plan settled both, which is the ordinary case and costs one
2950/// pass over the plan's seams.
2951#[must_use]
2952pub fn refused_upstream(
2953    c: &Campaign,
2954    node: &NodeId,
2955    resolved: &[PlacedSeam],
2956    reads: &mut Reads,
2957) -> String {
2958    let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
2959        return String::new();
2960    };
2961    let mut parts: Vec<String> = Vec::new();
2962
2963    if let Some(q) = Metrics::table().grid(reads).map(|g| g.quantum)
2964        && let Some(b) = plan.boxes.iter().find(|b| &b.node == node)
2965    {
2966        let axes: Vec<&str> = [(0usize, "x"), (1usize, "z")]
2967            .into_iter()
2968            .filter(|(a, _)| off_grid(b.extent[*a].get(), q))
2969            .map(|(_, name)| name)
2970            .collect();
2971        if !axes.is_empty() {
2972            parts.push(format!(
2973                "this place's box is off the kit grid on {axes} and `DW0825` has already refused \
2974                 it, so the frame above is not one this map keeps",
2975                axes = axes.join(" and "),
2976            ));
2977        }
2978    }
2979
2980    // A seam the plan wrote and did not resolve: the graph edge names this
2981    // place, the plan has a seam row for it, and nothing came out the other end.
2982    let graph_edges: BTreeMap<&str, &Edge> = c
2983        .layout_graph
2984        .as_ref()
2985        .map(|g| {
2986            g.content
2987                .edges
2988                .iter()
2989                .map(|e| (e.id().0.as_str(), e))
2990                .collect()
2991        })
2992        .unwrap_or_default();
2993    let unresolved: Vec<String> = plan
2994        .seams
2995        .iter()
2996        .filter(|s| {
2997            graph_edges
2998                .get(s.edge.0.as_str())
2999                .is_some_and(|e| e.a() == node || e.b() == node)
3000                && !resolved.iter().any(|r| r.edge == s.edge)
3001        })
3002        .map(|s| format!("`{}`", s.edge))
3003        .collect();
3004    if !unresolved.is_empty() {
3005        parts.push(format!(
3006            "the plan writes {n} seam(s) on this place that it does not resolve ({list}), which \
3007             `DW0828` or `DW0829` has already refused, so the allocation this piece is answering \
3008             is short of what the plan says",
3009            n = unresolved.len(),
3010            list = unresolved.join(", "),
3011        ));
3012    }
3013
3014    if parts.is_empty() {
3015        return String::new();
3016    }
3017    format!(
3018        " This measurement stands downstream of a site-plan refusal: {parts}. Repair the plan \
3019         first — this line moves with it.",
3020        parts = parts.join("; "),
3021    )
3022}
3023
3024/// `DW0826`: nothing the plan places leaves the region.
3025///
3026/// **The region is the brief's number flowing down.** A box is never grounds to
3027/// grow it: the prescription in the message is to move or shrink the box, or to
3028/// change the brief's fact and re-derive *visibly* — never to let the extent be
3029/// whatever the parts added up to, which is the failure this whole stage was
3030/// bought to end.
3031///
3032/// Whole-owned volumes answer to it too. A `massif` outside the region is the
3033/// whole owning mass beyond its own declared extent, which is the same
3034/// extent-flows-up defect arriving through the back door.
3035///
3036/// **One region number is one finding, however many boxes stand outside it**
3037/// (`crate::diagnostic`'s "one cause, one line"). The region is a single fact
3038/// handed down by the brief, so a region a course too short puts *every* box
3039/// over the same edge and each per-box refusal prints the same two numbers with
3040/// a different name in front: measured on a 24-place campaign, shortening the
3041/// region by five courses printed 24 identical paragraphs. When more than one
3042/// thing leaves it, this states the count, names every offender with its own
3043/// overrun, and prescribes once. A single offender still gets its own line
3044/// exactly as before — the fold is reachable only where the copies would have
3045/// been.
3046fn region(plan: &SitePlanContent, placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
3047    let r = &plan.region;
3048    let spans = [region_span(r, 0), region_span(r, 1), region_span(r, 2)];
3049    // How the region reads once, for the folded arms: the per-item clause
3050    // repeats it, and repeating it N times is most of what made N copies
3051    // unreadable.
3052    let region_text = format!(
3053        "x {}..{}, y {}..{}, z {}..{}",
3054        spans[0].0, spans[0].1, spans[1].0, spans[1].1, spans[2].0, spans[2].1
3055    );
3056
3057    // ---- boxes ----
3058    let mut boxes_out: Vec<Overrun<'_>> = Vec::new();
3059    for p in placed {
3060        let mut bad: Vec<(&'static str, i64, i64)> = Vec::new();
3061        if !within((p.x0(), p.x1()), spans[0]) {
3062            bad.push(("x", p.x0(), p.x1()));
3063        }
3064        if let Some(y) = p.y_span()
3065            && !within(y, spans[1])
3066        {
3067            bad.push(("y", y.0, y.1));
3068        }
3069        if !within((p.z0(), p.z1()), spans[2]) {
3070            bad.push(("z", p.z0(), p.z1()));
3071        }
3072        if !bad.is_empty() {
3073            boxes_out.push(Overrun {
3074                index: p.index,
3075                name: p.plan.node.0.as_str(),
3076                axes: bad,
3077                by: p.by.describe(),
3078            });
3079        }
3080    }
3081    if boxes_out.len() == 1 {
3082        let o = &boxes_out[0];
3083        d.push(Diagnostic::error(
3084            DW_BOX_LEAVES_REGION,
3085            "site-plan",
3086            format!("/content/boxes/{}", o.index),
3087            format!(
3088                "box for `{node}` leaves the region: {bad}. It stands where it stands because it \
3089                 is {by}. The region is the whole map's extent, and it comes from the brief — a \
3090                 box is never grounds to grow it. Move the box (its pin, or the offsets of the \
3091                 seam that placed it), shrink it, or change the brief's fact and re-derive the \
3092                 region so the change is visible in the document that owns it.",
3093                by = o.by,
3094                node = o.name,
3095                bad = against_region(&o.axes, &spans),
3096            ),
3097        ));
3098    } else if boxes_out.len() > 1 {
3099        d.push(Diagnostic::error(
3100            DW_BOX_LEAVES_REGION,
3101            "site-plan",
3102            "/content/boxes",
3103            format!(
3104                "{n} of the {total} box(es) this plan places leave the region, which is \
3105                 {region_text}: {list}. The region is the whole map's extent, and it comes from \
3106                 the brief — a box is never grounds to grow it. One region is the cause of all \
3107                 {n} of these, which is why they are one line and not {n}: move or shrink the \
3108                 boxes, or change the brief's fact and re-derive the region so the change is \
3109                 visible in the document that owns it.",
3110                n = boxes_out.len(),
3111                total = placed.len(),
3112                list = named_overruns(&boxes_out),
3113            ),
3114        ));
3115    }
3116
3117    // ---- whole-owned volumes ----
3118    let mut volumes_out: Vec<Overrun<'_>> = Vec::new();
3119    for (i, v) in plan.volumes.iter().enumerate() {
3120        let vmax = v.region.max();
3121        let mut bad: Vec<(&'static str, i64, i64)> = Vec::new();
3122        for (axis, name) in [(0usize, "x"), (1, "y"), (2, "z")] {
3123            if !within((v.region.min[axis], vmax[axis]), spans[axis]) {
3124                bad.push((name, v.region.min[axis], vmax[axis]));
3125            }
3126        }
3127        if !bad.is_empty() {
3128            volumes_out.push(Overrun {
3129                by: String::new(),
3130                index: i,
3131                name: v.id.0.as_str(),
3132                axes: bad,
3133            });
3134        }
3135    }
3136    if volumes_out.len() == 1 {
3137        let o = &volumes_out[0];
3138        d.push(Diagnostic::error(
3139            DW_BOX_LEAVES_REGION,
3140            "site-plan",
3141            format!("/content/volumes/{}", o.index),
3142            format!(
3143                "whole-owned volume `{id}` leaves the region: {bad}. The region is the whole's \
3144                 own extent; mass outside it is the whole growing to fit what was put in it, \
3145                 which is the direction this stage exists to forbid.",
3146                id = o.name,
3147                bad = against_region(&o.axes, &spans),
3148            ),
3149        ));
3150    } else if volumes_out.len() > 1 {
3151        d.push(Diagnostic::error(
3152            DW_BOX_LEAVES_REGION,
3153            "site-plan",
3154            "/content/volumes",
3155            format!(
3156                "{n} of the {total} whole-owned volume(s) leave the region, which is \
3157                 {region_text}: {list}. The region is the whole's own extent; mass outside it is \
3158                 the whole growing to fit what was put in it, which is the direction this stage \
3159                 exists to forbid. One region is the cause of all {n} of these, which is why \
3160                 they are one line and not {n}.",
3161                n = volumes_out.len(),
3162                total = plan.volumes.len(),
3163                list = named_overruns(&volumes_out),
3164            ),
3165        ));
3166    }
3167}
3168
3169/// One offender's overrun, spelled against the region on each axis it leaves —
3170/// the wording a single finding carries, where the region's own numbers are
3171/// stated beside the box's because there is only one line to read them in.
3172fn against_region(bad: &[(&'static str, i64, i64)], spans: &[(i64, i64); 3]) -> String {
3173    bad.iter()
3174        .map(|(name, lo, hi)| {
3175            let axis = match *name {
3176                "x" => 0,
3177                "y" => 1,
3178                _ => 2,
3179            };
3180            format!(
3181                "{name} {lo}..{hi} against the region's {}..{}",
3182                spans[axis].0, spans[axis].1
3183            )
3184        })
3185        .collect::<Vec<_>>()
3186        .join("; ")
3187}
3188
3189/// One thing the plan places that stands outside the region, and where.
3190///
3191/// A named struct rather than a tuple because both arms read it: the single
3192/// finding addresses its own `index`, the folded one prints `name` and `axes`,
3193/// and boxes and volumes differ in nothing else.
3194struct Overrun<'a> {
3195    /// Position in its own array — the path a single finding is addressed at.
3196    index: usize,
3197    /// The id an author reads.
3198    name: &'a str,
3199    /// Each axis it leaves, with its own inclusive span on that axis.
3200    axes: Vec<(&'static str, i64, i64)>,
3201    /// How its corner was obtained, in the words a refusal uses.
3202    by: String,
3203}
3204
3205/// The offender list a folded finding carries: every name with its own overrun,
3206/// and no repeat of the region — the folded message states that once.
3207fn named_overruns(items: &[Overrun<'_>]) -> String {
3208    items
3209        .iter()
3210        .map(|o| {
3211            format!(
3212                "`{}` ({})",
3213                o.name,
3214                o.axes
3215                    .iter()
3216                    .map(|(ax, lo, hi)| format!("{ax} {lo}..{hi}"))
3217                    .collect::<Vec<_>>()
3218                    .join(", ")
3219            )
3220        })
3221        .collect::<Vec<_>>()
3222        .join(", ")
3223}
3224
3225/// `DW0827`: the boxes are disjoint.
3226///
3227/// Shared **faces** are the only permitted contact, because a seam needs one —
3228/// and a shared face is a one-cell gap, not a touch (see [`Placed`]). Two boxes
3229/// whose play spaces meet are two authorities over one cell, which the
3230/// derivation would have to arbitrate and must never be asked to.
3231fn disjoint(placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
3232    for (a_i, a) in placed.iter().enumerate() {
3233        for b in &placed[a_i + 1..] {
3234            let (Some(x), Some(z)) = (
3235                overlap((a.x0(), a.x1()), (b.x0(), b.x1())),
3236                overlap((a.z0(), a.z1()), (b.z0(), b.z1())),
3237            ) else {
3238                continue;
3239            };
3240            let y = match (a.y_span(), b.y_span()) {
3241                (Some(ya), Some(yb)) => match overlap(ya, yb) {
3242                    Some(y) => y,
3243                    None => continue,
3244                },
3245                // One of them is sky-open with an unresolved class; `DW0812`
3246                // owns that name, and the footprints alone are enough to say
3247                // the two places stand in each other.
3248                _ => (a.floor.min(b.floor), a.floor.max(b.floor)),
3249            };
3250            d.push(Diagnostic::error(
3251                DW_BOXES_OVERLAP,
3252                "site-plan",
3253                format!("/content/boxes/{}", b.index),
3254                format!(
3255                    "the boxes for `{a_n}` and `{b_n}` overlap, sharing x {x0}..{x1}, y \
3256                     {y0}..{y1}, z {z0}..{z1}. Two places may share a FACE — that is what a seam \
3257                     is cut through — but never a cell: overlapping boxes are two owners for one \
3258                     block, and the derivation would have to pick between them with no rule to \
3259                     pick by. Boxes that connect sit one cell apart, and the cell between them \
3260                     is the wall they have in common.",
3261                    a_n = a.plan.node,
3262                    b_n = b.plan.node,
3263                    x0 = x.0,
3264                    x1 = x.1,
3265                    y0 = y.0,
3266                    y1 = y.1,
3267                    z0 = z.0,
3268                    z1 = z.1,
3269                ),
3270            ));
3271        }
3272    }
3273}
3274
3275/// `DW0835`: the whole's mass stands beside, under and over places — never
3276/// inside one.
3277fn volumes_outside_boxes(plan: &SitePlanContent, placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
3278    for (i, v) in plan.volumes.iter().enumerate() {
3279        let vmax = v.region.max();
3280        for p in placed {
3281            let (Some(x), Some(z)) = (
3282                overlap((v.region.min[0], vmax[0]), (p.x0(), p.x1())),
3283                overlap((v.region.min[2], vmax[2]), (p.z0(), p.z1())),
3284            ) else {
3285                continue;
3286            };
3287            let Some(py) = p.y_span() else { continue };
3288            let Some(y) = overlap((v.region.min[1], vmax[1]), py) else {
3289                continue;
3290            };
3291            d.push(Diagnostic::error(
3292                DW_VOLUME_IN_BOX,
3293                "site-plan",
3294                format!("/content/volumes/{i}"),
3295                format!(
3296                    "whole-owned volume `{id}` ({role}) enters the box for `{node}`, sharing x \
3297                     {x0}..{x1}, y {y0}..{y1}, z {z0}..{z1}. The whole's mass may stand beside a \
3298                     place, under it and over it; inside it, the volume and the place are two \
3299                     authorities writing one cell, and the derivation must never be asked to \
3300                     arbitrate that. Pull the volume back to the place's face, or move the \
3301                     place.",
3302                    id = v.id,
3303                    role = v.role.as_str(),
3304                    node = p.plan.node,
3305                    x0 = x.0,
3306                    x1 = x.1,
3307                    y0 = y.0,
3308                    y1 = y.1,
3309                    z0 = z.0,
3310                    z1 = z.1,
3311                ),
3312            ));
3313        }
3314    }
3315}
3316
3317/// `DW0832`: a box is built to its place's class — **either kind** (spec-0053
3318/// §3).
3319///
3320/// # The way branch, and why its third demand is structural
3321///
3322/// A size class bounds both horizontal extents and this is the one place that
3323/// becomes geometry. A way class bounds only the **cross-section**, which is the
3324/// box's *shorter* horizontal extent — the axis a body feels — and then demands
3325/// that the **run**, the longer extent, strictly EXCEED the class's
3326/// `max_width`.
3327///
3328/// That third demand is the elongation, and it is deliberately derived from the
3329/// class's own widest cross-section rather than seeded as a constant, because it
3330/// is exactly what a room cannot supply. A square box can never satisfy it: its
3331/// run equals its width, and one number cannot both be `<= max_width` and exceed
3332/// it. So "declare a room a way to escape the size ladder" is refused **by the
3333/// object's own shape** rather than by a rule the author could satisfy by
3334/// choosing differently — the property `CLAUDE.md` demands of an opt-out, since
3335/// the defect this branch exists to catch is structurally incapable of
3336/// producing its proof.
3337///
3338/// There is no maximum run and there is not going to be one: a route's length is
3339/// per-campaign geometry, never a standard (spec-0053 §7).
3340fn size_classes(placed: &[Placed<'_>], d: &mut Vec<Diagnostic>) {
3341    for p in placed {
3342        let Some(class) = p.class else {
3343            continue; // `DW0812` refused the name.
3344        };
3345        let (kind, mut bad) = match class {
3346            PlaceClass::Size(sc) => {
3347                let mut bad: Vec<String> = Vec::new();
3348                for (axis, name) in [(0usize, "x"), (1, "z")] {
3349                    let e = p.plan.extent[axis].get();
3350                    if e < sc.min_footprint[axis] || e > sc.max_footprint[axis] {
3351                        bad.push(format!(
3352                            "{e} blocks on {name}, outside the class's {}..{}",
3353                            sc.min_footprint[axis], sc.max_footprint[axis]
3354                        ));
3355                    }
3356                }
3357                ("size", bad)
3358            }
3359            PlaceClass::Way(w) => {
3360                let mut bad: Vec<String> = Vec::new();
3361                let (dx, dz) = (p.plan.extent[0].get(), p.plan.extent[1].get());
3362                let (width, run) = (dx.min(dz), dx.max(dz));
3363                let axis = if dx <= dz { "x" } else { "z" };
3364                if width < w.min_width || width > w.max_width {
3365                    bad.push(format!(
3366                        "a cross-section of {width} blocks (its shorter extent, on {axis}), \
3367                         outside the class's {}..{}",
3368                        w.min_width, w.max_width
3369                    ));
3370                }
3371                if run <= w.max_width {
3372                    bad.push(format!(
3373                        "a run of {run} blocks, which does not exceed the class's widest \
3374                         cross-section of {}. A way is a place that is longer than it is wide \
3375                         by kind and not by margin, so this box is a room — give it a \
3376                         `size_class` instead, or make it longer",
3377                        w.max_width
3378                    ));
3379                }
3380                ("way", bad)
3381            }
3382        };
3383        if let Ceiling::Clearance(c) = p.plan.ceiling
3384            && c.get() < class.min_clearance()
3385        {
3386            bad.push(format!(
3387                "{c} cells of headroom, under the class's minimum of {}",
3388                class.min_clearance()
3389            ));
3390        }
3391        if bad.is_empty() {
3392            continue;
3393        }
3394        d.push(Diagnostic::error(
3395            DW_SIZE_CLASS,
3396            "site-plan",
3397            format!("/content/boxes/{}", p.index),
3398            format!(
3399                "the box for `{node}` is not built to its declared {kind} class: {bad}. The \
3400                 class is the vocabulary the graph chose this place's scale in, and this is the \
3401                 one place it becomes geometry — either build the box to it, or declare the \
3402                 place a different class in the layout graph and say so there.",
3403                node = p.plan.node,
3404                bad = bad.join("; "),
3405            ),
3406        ));
3407    }
3408}
3409
3410// ---------------------------------------------------------------------------
3411// Seams: allocated on a face both boxes already have
3412// ---------------------------------------------------------------------------
3413
3414/// The rectangle two boxes have in common on one face, in the face's own two
3415/// in-plane world axes, plus the plane the wall between them stands in.
3416#[derive(Debug, Clone, Copy)]
3417struct SharedFace {
3418    plane: i64,
3419    u: (i64, i64),
3420    v: (i64, i64),
3421    u_axis: &'static str,
3422    v_axis: &'static str,
3423}
3424
3425/// Why two boxes do not share the declared face.
3426#[derive(Debug, Clone)]
3427enum NotShared {
3428    /// They are not neighbours across it: the gap is `gap` cells where the one
3429    /// wall they would have in common needs exactly 1.
3430    NotAdjacent { gap: i64 },
3431    /// They are neighbours, but the face they would share is empty because their
3432    /// spans miss each other on one of the two in-plane axes.
3433    NoCommonArea { axis: &'static str },
3434    /// One of them is sky-open with no stated headroom, so it has no ceiling or
3435    /// floor plane for a horizontal seam to sit in.
3436    NoPlane { which: &'static str },
3437}
3438
3439/// Do these two boxes share `face` **of `a`**, and where?
3440///
3441/// A shared face is a **one-cell gap** — the wall the two places have in common,
3442/// which the derivation writes once. See [`Placed`] for why the box is the play
3443/// space rather than the play space plus its shell.
3444/// The geometry a shared-face question needs of one box: its footprint and its
3445/// vertical span, when it has one.
3446///
3447/// A tiny value rather than `&Placed` so that the **one** implementation of "do
3448/// these two boxes share this face" serves both readers of the resolved plan:
3449/// the stage-4 checks, which hold a partially-resolved box, and the derivation
3450/// and battery in the compiler, which hold a [`PlacedBox`]. A second copy of
3451/// this arithmetic is how a plan-time green and a byte-time green come to be
3452/// about different walls.
3453#[derive(Clone, Copy)]
3454struct FaceSide {
3455    foot: [i64; 4],
3456    y: Option<(i64, i64)>,
3457}
3458
3459impl Placed<'_> {
3460    fn side(&self) -> FaceSide {
3461        FaceSide {
3462            foot: self.foot,
3463            y: self.y_span(),
3464        }
3465    }
3466}
3467
3468impl PlacedBox {
3469    fn side(&self) -> FaceSide {
3470        let (lo, hi) = self.space();
3471        FaceSide {
3472            foot: self.foot,
3473            y: Some((lo[1], hi[1])),
3474        }
3475    }
3476}
3477
3478/// [`shared_face`] over two fully resolved boxes.
3479fn shared_face_of(a: &PlacedBox, b: &PlacedBox, face: Face) -> Result<SharedFace, NotShared> {
3480    shared_face(a.side(), b.side(), face)
3481}
3482
3483fn shared_face(a: FaceSide, b: FaceSide, face: Face) -> Result<SharedFace, NotShared> {
3484    let horizontal_pair = |plane: i64, u: (i64, i64), v: (i64, i64)| -> SharedFace {
3485        SharedFace {
3486            plane,
3487            u,
3488            v,
3489            u_axis: "x",
3490            v_axis: "z",
3491        }
3492    };
3493    match face {
3494        Face::East | Face::West | Face::South | Face::North => {
3495            // The axis the face's normal runs along, and the horizontal axis
3496            // that stays in the plane.
3497            let (normal, along) = match face {
3498                Face::East | Face::West => (0usize, 2usize),
3499                _ => (2usize, 0usize),
3500            };
3501            let a_span = span(a.foot, normal);
3502            let b_span = span(b.foot, normal);
3503            let positive = matches!(face, Face::East | Face::South);
3504            let (plane, gap) = if positive {
3505                (a_span.1 + 1, b_span.0 - a_span.1 - 1)
3506            } else {
3507                (a_span.0 - 1, a_span.0 - b_span.1 - 1)
3508            };
3509            if gap != SHARED_FACE_GAP_CELLS {
3510                return Err(NotShared::NotAdjacent { gap });
3511            }
3512            let a_along = span(a.foot, along);
3513            let b_along = span(b.foot, along);
3514            let u = overlap(a_along, b_along).ok_or(NotShared::NoCommonArea {
3515                axis: if along == 0 { "x" } else { "z" },
3516            })?;
3517            let (ya, yb) = match (a.y, b.y) {
3518                (Some(ya), Some(yb)) => (ya, yb),
3519                (None, _) => return Err(NotShared::NoPlane { which: "a" }),
3520                (_, None) => return Err(NotShared::NoPlane { which: "b" }),
3521            };
3522            let v = overlap(ya, yb).ok_or(NotShared::NoCommonArea { axis: "y" })?;
3523            Ok(SharedFace {
3524                plane,
3525                u,
3526                v,
3527                u_axis: if along == 0 { "x" } else { "z" },
3528                v_axis: "y",
3529            })
3530        }
3531        Face::Up | Face::Down => {
3532            let (Some(ya), Some(yb)) = (a.y, b.y) else {
3533                return Err(NotShared::NoPlane {
3534                    which: if a.y.is_none() { "a" } else { "b" },
3535                });
3536            };
3537            let (plane, gap) = if face == Face::Up {
3538                (ya.1 + 1, yb.0 - ya.1 - 1)
3539            } else {
3540                (ya.0 - 1, ya.0 - yb.1 - 1)
3541            };
3542            if gap != SHARED_FACE_GAP_CELLS {
3543                return Err(NotShared::NotAdjacent { gap });
3544            }
3545            let u = overlap(span(a.foot, 0), span(b.foot, 0))
3546                .ok_or(NotShared::NoCommonArea { axis: "x" })?;
3547            let v = overlap(span(a.foot, 2), span(b.foot, 2))
3548                .ok_or(NotShared::NoCommonArea { axis: "z" })?;
3549            Ok(horizontal_pair(plane, u, v))
3550        }
3551    }
3552}
3553
3554/// One seam and everything already resolved about it: the two places it joins,
3555/// the connection it allocates, and the face they share. Carried as one value so
3556/// each rule below takes the seam and its context rather than eight positional
3557/// arguments — the shape a `clippy::too_many_arguments` allow would otherwise
3558/// have papered over.
3559struct SeamCtx<'a> {
3560    index: usize,
3561    seam: &'a Seam,
3562    edge: &'a Edge,
3563    a: &'a Placed<'a>,
3564    b: &'a Placed<'a>,
3565    face: SharedFace,
3566    /// The crossing's low corner on the face's own two world axes, from the
3567    /// packing — `[along, sill]` on a wall, `[x, z]` through a floor.
3568    at: [i64; 2],
3569}
3570
3571/// `DW0828`–`DW0831`: every seam sits on a face its two boxes share, at cells
3572/// that face has, through a standard opening a body can use, and — where the two
3573/// places are on different planes — by a climb or a fall the standards allow.
3574fn seams(
3575    plan: &SitePlanContent,
3576    graph: &LayoutGraphContent,
3577    placed: &[Placed<'_>],
3578    packed: &Packed,
3579    table: &Metrics,
3580    reads: &mut Reads,
3581    d: &mut Vec<Diagnostic>,
3582) {
3583    let by_node: BTreeMap<&str, &Placed<'_>> =
3584        placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
3585    let edges: BTreeMap<&str, &Edge> = graph.edges.iter().map(|e| (e.id().0.as_str(), e)).collect();
3586
3587    for (i, s) in plan.seams.iter().enumerate() {
3588        if packed.refused.contains(&i) {
3589            continue; // the packing refused this seam by name already.
3590        }
3591        let Some(edge) = edges.get(s.edge.0.as_str()) else {
3592            continue; // `DW0824` refused the reference.
3593        };
3594        if !edge.has_seam() {
3595            continue; // `DW0824` said this carries a sightline.
3596        }
3597        let (Some(a), Some(b)) = (
3598            by_node.get(edge.a().0.as_str()).copied(),
3599            by_node.get(edge.b().0.as_str()).copied(),
3600        ) else {
3601            continue; // `DW0824` reported the missing box, or nothing placed it.
3602        };
3603        let Some(at) = packed.seam_at[i] else {
3604            continue; // an end has no plane; `DW0112` said so.
3605        };
3606
3607        let face = match shared_face(a.side(), b.side(), s.face) {
3608            Ok(f) => f,
3609            Err(why) => {
3610                d.push(not_shared(i, s, edge, a, b, &why));
3611                continue;
3612            }
3613        };
3614
3615        let ctx = SeamCtx {
3616            index: i,
3617            seam: s,
3618            edge,
3619            a,
3620            b,
3621            face,
3622            at,
3623        };
3624
3625        // `DW0876`, first: a seam that does not state exactly one kind of
3626        // connection has no crossing for any rule below to judge, and telling
3627        // an author both that and what the crossing they did not state would
3628        // have meant prescribes two repairs for one mistake.
3629        if !contact_declaration(&ctx, table, reads, d) {
3630            continue;
3631        }
3632
3633        let opening = if s.contact.is_some() {
3634            // A contact has no opening name to resolve and no single sill, so
3635            // `DW0829` does not run over it. That is stated rather than
3636            // shoehorned: calling a 55-cell front a door would make every
3637            // downstream door check wrong (spec-0053 §4).
3638            None
3639        } else {
3640            match table.resolve(
3641                MetricKind::Opening,
3642                s.opening.as_deref().unwrap_or_default(),
3643            ) {
3644                Ok(e) => match e.value(reads) {
3645                    MetricValue::Opening(o) => Some(*o),
3646                    _ => continue,
3647                },
3648                Err(unknown) => {
3649                    d.push(unknown.diagnostic("site-plan", &format!("/content/seams/{i}/opening")));
3650                    continue;
3651                }
3652            }
3653        };
3654
3655        if let Some(opening) = opening {
3656            opening_fits(&ctx, opening, d);
3657        }
3658        match edge {
3659            Edge::Stair { .. } => stair(&ctx, table, reads, d),
3660            Edge::Drop { falls, .. } => drop_seam(&ctx, *falls, table, reads, d),
3661            Edge::Walk { .. } | Edge::Barred { .. } => {
3662                if let Some(opening) = opening {
3663                    sill(&ctx, opening, d);
3664                }
3665            }
3666            Edge::Carry { .. } | Edge::Vision { .. } => {}
3667        }
3668        if matches!(edge, Edge::Stair { .. }) && s.stair_in.is_none() {
3669            d.push(Diagnostic::error(
3670                DW_STAIR_PITCH,
3671                "site-plan",
3672                format!("/content/seams/{i}"),
3673                format!(
3674                    "the seam for stair `{id}` does not say which place hosts its treads. A \
3675                     stair is massing, and massing stands somewhere: name `{a}` or `{b}` in \
3676                     `stair_in`, so that the run it costs comes out of a footprint the plan has \
3677                     already allocated rather than out of whatever space happens to be left.",
3678                    id = s.edge,
3679                    a = edge.a(),
3680                    b = edge.b(),
3681                ),
3682            ));
3683        }
3684    }
3685}
3686
3687/// `DW0828`, with the arithmetic that produced it.
3688fn not_shared(
3689    i: usize,
3690    s: &Seam,
3691    edge: &Edge,
3692    a: &Placed<'_>,
3693    b: &Placed<'_>,
3694    why: &NotShared,
3695) -> Diagnostic {
3696    let detail = match why {
3697        NotShared::NotAdjacent { gap } if *gap < 0 => format!(
3698            "they overlap by {} cell(s) across it rather than standing one apart",
3699            -gap
3700        ),
3701        NotShared::NotAdjacent { gap } => format!(
3702            "there are {gap} cells between them across that face where a shared wall is exactly \
3703             {SHARED_FACE_GAP_CELLS}"
3704        ),
3705        NotShared::NoCommonArea { axis } => format!(
3706            "they are neighbours across it, but their spans on {axis} miss each other entirely, \
3707             so the face they share has no area to cut an opening in"
3708        ),
3709        NotShared::NoPlane { which } => format!(
3710            "the `{which}` end is sky-open with no stated headroom, so it has no ceiling or floor \
3711             plane for a horizontal seam to sit in"
3712        ),
3713    };
3714    Diagnostic::error(
3715        DW_SEAM_NOT_SHARED,
3716        "site-plan",
3717        format!("/content/seams/{i}/face"),
3718        format!(
3719            "the seam for `{id}` is declared on the {face} face of `{an}`, and `{an}` and `{bn}` \
3720             do not share it: {detail}. **A seam is allocated on a face both boxes already \
3721             have** — that is the whole of why the plan places it while both are still free to \
3722             move, instead of two finished places discovering later that they cannot mate. \
3723             `{an}` is x {ax0}..{ax1}, z {az0}..{az1} at floor {af}; `{bn}` is x {bx0}..{bx1}, \
3724             z {bz0}..{bz1} at floor {bf}. Move one box against the other, or put the seam on \
3725             the face they really share.",
3726            id = s.edge,
3727            face = s.face.as_str(),
3728            an = edge.a(),
3729            bn = edge.b(),
3730            ax0 = a.x0(),
3731            ax1 = a.x1(),
3732            az0 = a.z0(),
3733            az1 = a.z1(),
3734            af = a.floor,
3735            bx0 = b.x0(),
3736            bx1 = b.x1(),
3737            bz0 = b.z0(),
3738            bz1 = b.z1(),
3739            bf = b.floor,
3740        ),
3741    )
3742}
3743
3744/// **`DW0876`**: this seam states exactly one kind of
3745/// connection, and if it is a contact, one this engine builds (spec-0053 §4).
3746///
3747/// Returns `false` when the seam has no usable crossing, in which case the
3748/// caller stops: everything below reads the crossing rectangle.
3749fn contact_declaration(
3750    ctx: &SeamCtx<'_>,
3751    table: &Metrics,
3752    reads: &mut Reads,
3753    d: &mut Vec<Diagnostic>,
3754) -> bool {
3755    let (i, s) = (ctx.index, ctx.seam);
3756    let mut refuse = |what: String, remedy: String| {
3757        d.push(Diagnostic::error(
3758            DW_CONTACT,
3759            "site-plan",
3760            format!("/content/seams/{i}"),
3761            format!(
3762                "the seam for `{edge}` {what}. To fix it, {remedy}.",
3763                edge = s.edge,
3764            ),
3765        ));
3766    };
3767
3768    // ---- Shape 1: exactly one kind.
3769    match (s.opening.as_ref(), s.contact.as_ref()) {
3770        (Some(o), Some(_)) => {
3771            refuse(
3772                format!(
3773                    "declares BOTH an `opening` (`{o}`) and a `contact` — a hand-off is one \
3774                     kind or the other"
3775                ),
3776                "delete whichever this is not. A portal allocates the cells a body crosses \
3777                 at and every one of them must be passable; a contact is a front along which \
3778                 two places simply meet and needs only one crossable column. The derivation \
3779                 builds them differently and the byte observer measures them differently, so \
3780                 there is no world in which a seam is both"
3781                    .to_string(),
3782            );
3783            return false;
3784        }
3785        (None, None) => {
3786            refuse(
3787                "declares neither an `opening` nor a `contact`, so it states no way across"
3788                    .to_string(),
3789                format!(
3790                    "give it one. A doorway is `\"opening\": \"<name>\"` — defined \
3791                     standards: {names}. A front where the two places simply meet is \
3792                     `\"contact\": {{}}`, which spans from `at` to the far edge of the \
3793                     shared face",
3794                    names = table.names_of(MetricKind::Opening).join(", "),
3795                ),
3796            );
3797            return false;
3798        }
3799        (Some(_), None) => return true,
3800        (None, Some(_)) => {}
3801    }
3802
3803    // ---- Shape 4: the classes a contact may carry.
3804    //
3805    // `walk` and `drop` only. A rim falling to a lower court is a genuine broad
3806    // hand-off, so `drop` is in; `stair`, `barred` and `vision` are excluded
3807    // until a campaign brief demands one (spec-0053 §4, the falsifier re-armed).
3808    if !matches!(ctx.edge, Edge::Walk { .. } | Edge::Drop { .. }) {
3809        refuse(
3810            format!(
3811                "is a contact on a `{class}` connection, and a contact carries `walk` or \
3812                 `drop` only",
3813                class = ctx.edge.class(),
3814            ),
3815            "give the seam a standard `opening`, or declare the connection `walk` or \
3816             `drop` in the layout graph. A stair needs a run and a pitch, a barred door \
3817             needs a gate region that seals and clears, and a sightline is not a crossing \
3818             at all — none of the three is a thing a front can be, and this engine does \
3819             not have them as contacts until a campaign brief demands one"
3820                .to_string(),
3821        );
3822        return false;
3823    }
3824
3825    // ---- Shape 3: wider than the broadest standard opening.
3826    //
3827    // The floor is derived from the standard set, so anything at or under it
3828    // could have been a portal. That is what makes it a demand the defect
3829    // cannot supply: a door declared a contact is refused by its own width.
3830    let Some(floor) = table.broadest_opening_width(reads) else {
3831        return false; // `Metrics::self_check` reports a table with no openings.
3832    };
3833    let (u_span, v_span) = (ctx.face.u, ctx.face.v);
3834    let (u_hi, v_hi) = crossing_hi(ctx);
3835    let width = u_hi - ctx.at[0] + 1;
3836    if width <= i64::from(floor) {
3837        refuse(
3838            format!(
3839                "is a contact {width} cell(s) wide, which is not wider than the broadest \
3840                 standard opening ({floor} cells)"
3841            ),
3842            format!(
3843                "widen the span, or declare it a portal — anything this narrow could have \
3844                 been one, and a doorway called a contact would dodge the standard set \
3845                 while every downstream door check went on being wrong about it. Defined \
3846                 openings: {names}",
3847                names = table.names_of(MetricKind::Opening).join(", "),
3848            ),
3849        );
3850        return false;
3851    }
3852
3853    // ---- Shape 2: the span lies on the shared face.
3854    let mut off: Vec<String> = Vec::new();
3855    if ctx.at[0] < u_span.0 || u_hi > u_span.1 {
3856        off.push(format!(
3857            "{}..{} on {}, against the face's {}..{}",
3858            ctx.at[0], u_hi, ctx.face.u_axis, u_span.0, u_span.1
3859        ));
3860    }
3861    if ctx.at[1] < v_span.0 || v_hi > v_span.1 {
3862        off.push(format!(
3863            "{}..{} on {}, against the face's {}..{}",
3864            ctx.at[1], v_hi, ctx.face.v_axis, v_span.0, v_span.1
3865        ));
3866    }
3867    if !off.is_empty() {
3868        refuse(
3869            format!(
3870                "is a contact whose span leaves the face the two boxes share: {}",
3871                off.join("; ")
3872            ),
3873            "move `at` onto the shared face, or shorten `contact.extent` — the span is \
3874             where the derivation writes no wall, and a span running off the face would \
3875             ask it to open a wall that is not there. Omitting `contact.extent` runs the \
3876             span from `at` to the far edge of the face, which never leaves it"
3877                .to_string(),
3878        );
3879        return false;
3880    }
3881    true
3882}
3883
3884/// The far corner of a seam's crossing rectangle, on the face's own two in-plane
3885/// axes — [`contact_extent`] resolved against the seam's own anchor, so this
3886/// rule and the derivation describe one rectangle.
3887fn crossing_hi(ctx: &SeamCtx<'_>) -> (i64, i64) {
3888    let e = contact_extent(ctx.seam, ctx.at, &ctx.face);
3889    (ctx.at[0] + e[0] - 1, ctx.at[1] + e[1] - 1)
3890}
3891
3892/// `DW0828`'s anchor half and `DW0829`'s geometric half: the opening's cells are
3893/// cells the shared face has.
3894fn opening_fits(ctx: &SeamCtx<'_>, opening: crate::metrics::Opening, d: &mut Vec<Diagnostic>) {
3895    let (i, s, edge, face, at) = (ctx.index, ctx.seam, ctx.edge, &ctx.face, ctx.at);
3896    let anchor_in =
3897        at[0] >= face.u.0 && at[0] <= face.u.1 && at[1] >= face.v.0 && at[1] <= face.v.1;
3898    if !anchor_in {
3899        d.push(Diagnostic::error(
3900            DW_SEAM_NOT_SHARED,
3901            "site-plan",
3902            format!("/content/seams/{i}/at"),
3903            format!(
3904                "the seam for `{id}` is anchored at {ua} {u}, {va} {v}, which is not on the face \
3905                 `{an}` and `{bn}` share — that face runs {ua} {u0}..{u1} by {va} {v0}..{v1} in \
3906                 the plane at {plane}. `at` names the opening's low corner in the face's own two \
3907                 axes, so a corner off the face allocates the seam nowhere.",
3908                id = s.edge,
3909                an = edge.a(),
3910                bn = edge.b(),
3911                ua = face.u_axis,
3912                va = face.v_axis,
3913                u = at[0],
3914                v = at[1],
3915                u0 = face.u.0,
3916                u1 = face.u.1,
3917                v0 = face.v.0,
3918                v1 = face.v.1,
3919                plane = face.plane,
3920            ),
3921        ));
3922        return;
3923    }
3924    let u_hi = at[0] + i64::from(opening.width) - 1;
3925    let v_hi = at[1] + i64::from(opening.height) - 1;
3926    if u_hi <= face.u.1 && v_hi <= face.v.1 {
3927        return;
3928    }
3929    d.push(Diagnostic::error(
3930        DW_SEAM_OPENING,
3931        "site-plan",
3932        format!("/content/seams/{i}/opening"),
3933        format!(
3934            "the `{name}` opening ({w}x{h}) does not fit on the face `{an}` and `{bn}` share. \
3935             Anchored at {ua} {u}, {va} {v} it would run to {ua} {u_hi}, {va} {v_hi}, and the \
3936             shared face ends at {ua} {u1}, {va} {v1}. Move the anchor, choose a narrower \
3937             standard opening, or grow the overlap between the two boxes — the standard set is \
3938             the vocabulary, so the opening is never quietly cropped to fit.",
3939            name = s.opening.as_deref().unwrap_or_default(),
3940            w = opening.width,
3941            h = opening.height,
3942            an = edge.a(),
3943            bn = edge.b(),
3944            ua = face.u_axis,
3945            va = face.v_axis,
3946            u = at[0],
3947            v = at[1],
3948            u1 = face.u.1,
3949            v1 = face.v.1,
3950        ),
3951    ));
3952}
3953
3954/// `DW0829`'s step-rule half: a body standing on the floor of a side it enters
3955/// from can get onto the sill.
3956fn sill(ctx: &SeamCtx<'_>, opening: crate::metrics::Opening, d: &mut Vec<Diagnostic>) {
3957    let (i, s, edge, a, b, face) = (ctx.index, ctx.seam, ctx.edge, ctx.a, ctx.b, &ctx.face);
3958    if face.v_axis != "y" {
3959        return; // a horizontal seam has no sill; the fall or the treads own it.
3960    }
3961    let sources: Vec<(&NodeId, &Placed<'_>)> = match edge.direction() {
3962        Some(crate::layout::Direction::AToB) => vec![(edge.a(), a)],
3963        Some(crate::layout::Direction::BToA) => vec![(edge.b(), b)],
3964        None => vec![(edge.a(), a), (edge.b(), b)],
3965    };
3966    let max_rise = MAX_JUMP_RISE_16 / crate::metrics::FULL_16;
3967    for (name, p) in sources {
3968        let rise = ctx.at[1] - p.floor;
3969        if rise <= max_rise {
3970            continue;
3971        }
3972        d.push(Diagnostic::error(
3973            DW_SEAM_OPENING,
3974            "site-plan",
3975            format!("/content/seams/{i}/at"),
3976            format!(
3977                "the seam for `{id}` has its sill at y {sill}, {rise} blocks over the floor of \
3978                 `{name}` at y {floor}, and a body reaches at most {max_rise} block(s) by \
3979                 jumping ({j}/16 of vanilla's apex). A body entering from `{name}` cannot get \
3980                 into the opening at all, so the connection the graph declares is not one. The \
3981                 sill is the higher of the two floors: bring the floors within a step of each \
3982                 other, or declare the connection a `stair` and let the treads carry the climb. \
3983                 (The opening is {w}x{h}.)",
3984                id = s.edge,
3985                sill = ctx.at[1],
3986                j = MAX_JUMP_RISE_16,
3987                floor = p.floor,
3988                w = opening.width,
3989                h = opening.height,
3990            ),
3991        ));
3992    }
3993}
3994
3995/// `DW0830`: the stair the plan allocated can be built at a standard pitch,
3996/// inside the box the plan said hosts it.
3997fn stair(ctx: &SeamCtx<'_>, table: &Metrics, reads: &mut Reads, d: &mut Vec<Diagnostic>) {
3998    let (i, s, edge, a, b, face) = (ctx.index, ctx.seam, ctx.edge, ctx.a, ctx.b, &ctx.face);
3999    let rise = b.floor - a.floor;
4000    if rise == 0 {
4001        d.push(Diagnostic::error(
4002            DW_STAIR_PITCH,
4003            "site-plan",
4004            format!("/content/seams/{i}"),
4005            format!(
4006                "`{id}` is a stair, and `{an}` and `{bn}` are both on plane y {f} — so it climbs \
4007                 nothing. A stair's rise is not authored here: it is the difference between the \
4008                 two floors the plan has already chosen, which means a stair between two places \
4009                 at one level is a walk that has been called a stair. Move one floor, or declare \
4010                 the connection a `walk`.",
4011                id = s.edge,
4012                an = edge.a(),
4013                bn = edge.b(),
4014                f = a.floor,
4015            ),
4016        ));
4017        return;
4018    }
4019    let Some(host_id) = &s.stair_in else {
4020        return; // the missing declaration is reported by `seams`.
4021    };
4022    // **The treads stand in the LOWER place**, and that is geometry rather than
4023    // taste: a stair is a stack of courses rising off a walk plane, and the only
4024    // walk plane it can rise off is the lower of the two. Hosting it in the
4025    // upper place asks for a stack that starts at that place's floor and has to
4026    // reach a level *below* it, which is not a stair — it is a hole with treads
4027    // drawn in the air under it.
4028    //
4029    // Found by building. This code checked only that the host affords the RUN,
4030    // so a plan naming the upper place reached green at stage 4 and the
4031    // derivation then laid a mound on the wrong side of the opening; the
4032    // stage-5 observer caught it as a seam whose hole was still solid, which is
4033    // the right refusal for the wrong defect. `stair_in` stays authored rather
4034    // than derived because it says WHICH of the two footprints pays for the run
4035    // when both are candidates — but when only one can be, saying the other is
4036    // a refusal.
4037    let (low, high) = if b.floor > a.floor {
4038        (edge.a(), edge.b())
4039    } else {
4040        (edge.b(), edge.a())
4041    };
4042    if host_id == high {
4043        d.push(Diagnostic::error(
4044            DW_STAIR_PITCH,
4045            "site-plan",
4046            format!("/content/seams/{i}/stair_in"),
4047            format!(
4048                "the stair for `{id}` hosts its treads in `{high}`, which is the HIGHER of the two \
4049                 places (`{an}` stands at y {af}, `{bn}` at y {bf}). Treads rise off a walk plane, \
4050                 and the only plane this stair can rise off is the lower one — massing in the \
4051                 upper place would have to start at that place's floor and reach a level beneath \
4052                 it, which is not a stair. Host it in `{low}`, and check that `{low}` affords the \
4053                 run: a stair costs its footprint, and moving the host moves who pays.",
4054                id = s.edge,
4055                an = edge.a(),
4056                bn = edge.b(),
4057                af = a.floor,
4058                bf = b.floor,
4059            ),
4060        ));
4061        return;
4062    }
4063    let host = if host_id == edge.a() { a } else { b };
4064    // **The run is measured the way the derivation lays it**, by the function
4065    // that lays it — [`stair_run`]. What this check used to measure instead was
4066    // the seam's rise against the host's whole extent, and neither is what a
4067    // tread run costs: the courses carry a body to the OPENING, and through a
4068    // pierced floor they leave along one side of the hole. See [`stair_run`] for
4069    // the plan that reached green at `needs 8, affords 8` and built no stair.
4070    let Some((_, extent)) = crossing_rect(s, ctx.at, face, table, reads) else {
4071        return; // `DW0812` refused the opening; there is no rectangle to measure.
4072    };
4073    let Some(run) = stair_run(
4074        host.floor,
4075        host.foot,
4076        normal_axis_of(s.face),
4077        face.plane,
4078        crossing_aabb(s, ctx.at, face, extent),
4079    ) else {
4080        return; // no run to lay: the higher host is refused above, the stray hole by `DW0828`.
4081    };
4082    let run_axis = if run.run_axis == 0 { "x" } else { "z" };
4083    if gentlest_pitch(table, reads, run.climb, run.available).is_some() {
4084        return; // some standard pitch fits.
4085    }
4086    let Some((name, needed)) = tightest_pitch(table, reads, run.climb) else {
4087        return; // the table defines no pitch; `Metrics::self_check` owns that.
4088    };
4089    let carries = if run.climb == rise.abs() {
4090        String::new()
4091    } else {
4092        format!(
4093            " The treads carry {climb}, not {rise}: they rise off `{host_id}`'s floor at \
4094             {hf} and stop at {target}, which is where the opening puts a body.",
4095            climb = run.climb,
4096            rise = rise.abs(),
4097            hf = host.floor,
4098            target = host.floor + run.climb,
4099        )
4100    };
4101    d.push(Diagnostic::error(
4102        DW_STAIR_PITCH,
4103        "site-plan",
4104        format!("/content/seams/{i}"),
4105        format!(
4106            "the stair for `{id}` climbs {rise} block(s) between `{an}` (floor {af}) and `{bn}` \
4107             (floor {bf}), and no standard pitch fits inside `{host_id}`. The tightest standard \
4108             is `{name}`, which needs {needed} block(s) of run for a climb of {climb}, and \
4109             `{host_id}` affords {available} of run on {run_axis}.{carries} Give the host a \
4110             longer footprint on that axis, move the opening so the run has more room beside it, \
4111             host the stair in the other place, or bring the two floors closer together — the \
4112             pitches are standards, so a steeper one is not on offer.",
4113            id = s.edge,
4114            an = edge.a(),
4115            bn = edge.b(),
4116            af = a.floor,
4117            bf = b.floor,
4118            rise = rise.abs(),
4119            climb = run.climb,
4120            available = run.available,
4121        ),
4122    ));
4123}
4124
4125/// `DW0831`: a designed drop falls the way it says it falls, and no further than
4126/// the policy allows.
4127fn drop_seam(
4128    ctx: &SeamCtx<'_>,
4129    falls: crate::layout::Direction,
4130    table: &Metrics,
4131    reads: &mut Reads,
4132    d: &mut Vec<Diagnostic>,
4133) {
4134    let (i, s, edge, a, b) = (ctx.index, ctx.seam, ctx.edge, ctx.a, ctx.b);
4135    let (from, from_p, to, to_p) = match falls {
4136        crate::layout::Direction::AToB => (edge.a(), a, edge.b(), b),
4137        crate::layout::Direction::BToA => (edge.b(), b, edge.a(), a),
4138    };
4139    let depth = from_p.floor - to_p.floor;
4140    if depth <= 0 {
4141        d.push(Diagnostic::error(
4142            DW_DROP_POLICY,
4143            "site-plan",
4144            format!("/content/seams/{i}"),
4145            format!(
4146                "`{id}` falls from `{from}` (floor {ff}) into `{to}` (floor {tf}), which is \
4147                 {what}. A drop is one-way because a body cannot climb back up the way it came, \
4148                 and that is only true going down — this one is a mislabelled stair. Swap the \
4149                 declared direction, move the floors, or declare the connection a `stair`.",
4150                id = s.edge,
4151                ff = from_p.floor,
4152                tf = to_p.floor,
4153                what = if depth == 0 {
4154                    "the same plane".to_string()
4155                } else {
4156                    format!("{} block(s) HIGHER", -depth)
4157                },
4158            ),
4159        ));
4160        return;
4161    }
4162    let Some(cap) = table.max_designed_drop_blocks(reads) else {
4163        return;
4164    };
4165    if depth <= i64::from(cap) {
4166        return;
4167    }
4168    d.push(Diagnostic::error(
4169        DW_DROP_POLICY,
4170        "site-plan",
4171        format!("/content/seams/{i}"),
4172        format!(
4173            "`{id}` drops {depth} blocks from `{from}` into `{to}`, and the designed-drop policy \
4174             caps a declared fall at {cap}. This is a **policy** cap and it is deliberately far \
4175             tighter than what a body survives: a drop is a decision about the shape of the map, \
4176             and it should not also be a decision about the party's health. Bring the two floors \
4177             closer, or break the fall with a place between them.",
4178            id = s.edge,
4179        ),
4180    ));
4181}
4182
4183// ---------------------------------------------------------------------------
4184// Identities: the plan held to the brief's own numbers
4185// ---------------------------------------------------------------------------
4186
4187/// What one measure came out at, or why it could not be taken.
4188enum Measured {
4189    Value(f64),
4190    Unresolved(Diagnostic),
4191}
4192
4193/// `DW0833` and `DW0834`: the brief's numbers still hold once the boxes are
4194/// drawn, and the binding that holds them is not empty.
4195///
4196/// `DW0834` is a **warning**, not an error, and the difference is deliberate: a
4197/// deliberately minimal plan — a fixture, a first sketch — is a legitimate
4198/// thing to hold, and refusing it would make the smallest useful document
4199/// uncompilable. What it may not be is *silent*, so the empty side is named on
4200/// every run and is a finding for the round summary.
4201///
4202/// `DW0833` runs here over the plan. **Its second call site is the built
4203/// world**, where the same rule recomputes the same measures from assembled
4204/// bytes so that a derivation defect which moved a datum cannot hide behind a
4205/// plan-time green. That site belongs to the round that builds the blockout;
4206/// nothing here approximates it.
4207fn identities(
4208    c: &Campaign,
4209    plan: &SitePlanContent,
4210    placed: &[Placed<'_>],
4211    d: &mut Vec<Diagnostic>,
4212) {
4213    let facts: BTreeMap<&str, &crate::layout::BriefFact> = c
4214        .geometry_brief
4215        .as_ref()
4216        .map(|b| {
4217            b.content
4218                .facts
4219                .iter()
4220                .map(|f| (f.id.0.as_str(), f))
4221                .collect()
4222        })
4223        .unwrap_or_default();
4224
4225    if facts.is_empty() || plan.identities.is_empty() {
4226        let empty = match (facts.is_empty(), plan.identities.is_empty()) {
4227            (true, true) => "the brief states no fact and the plan declares no identity",
4228            (true, false) => "the brief states no fact",
4229            _ => "the plan declares no identity",
4230        };
4231        d.push(Diagnostic::warning(
4232            DW_IDENTITY_EMPTY,
4233            "site-plan",
4234            "/content/identities",
4235            format!(
4236                "the identity gate binds nothing: {empty}. This is what holds the whole map to \
4237                 the design somebody wrote down — with either side empty, the plan may say \
4238                 anything at all and every check above will still pass, because none of them \
4239                 has an opinion about how big the map was meant to be. It is a warning rather \
4240                 than a refusal so that a deliberately minimal plan stays compilable; it is \
4241                 printed every run so that the emptiness is never quietly a pass."
4242            ),
4243        ));
4244    }
4245
4246    let by_node: BTreeMap<&str, &Placed<'_>> =
4247        placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
4248    let datums: BTreeMap<&str, i64> = plan.datums.iter().map(|x| (x.id.0.as_str(), x.y)).collect();
4249
4250    for (i, id) in plan.identities.iter().enumerate() {
4251        let Some(fact) = facts.get(id.fact.0.as_str()) else {
4252            d.push(Diagnostic::error(
4253                DW_PLAN_AGREEMENT,
4254                "site-plan",
4255                format!("/content/identities/{i}/fact"),
4256                format!(
4257                    "this identity holds the map to `{f}`, which the geometry brief states no \
4258                     fact for. An identity binds to a number the brief WROTE DOWN — that is what \
4259                     makes it a design being kept rather than an assertion the plan makes about \
4260                     itself.",
4261                    f = id.fact,
4262                ),
4263            ));
4264            continue;
4265        };
4266        let measured = measure(&id.measure, plan, &by_node, &datums, i);
4267        let value = match measured {
4268            Measured::Value(v) => v,
4269            Measured::Unresolved(diag) => {
4270                d.push(diag);
4271                continue;
4272            }
4273        };
4274        if id.cmp.holds(value, fact.value) {
4275            continue;
4276        }
4277        d.push(Diagnostic::error(
4278            DW_IDENTITY_FALSE,
4279            "site-plan",
4280            format!("/content/identities/{i}"),
4281            format!(
4282                "the plan does not keep `{f}`: {what} measures {value}, and the brief asks for \
4283                 {cmp} {want}{unit}. The brief's sentence was: \"{note}\". Either move the \
4284                 geometry until the number is true, or change the brief's fact — in the brief, \
4285                 where the design is written down, so that the change is a decision somebody \
4286                 took rather than a plan that drifted.",
4287                f = id.fact,
4288                what = describe(&id.measure),
4289                cmp = id.cmp.as_str(),
4290                want = fact.value,
4291                unit = fact
4292                    .unit
4293                    .as_ref()
4294                    .map(|u| format!(" {u}"))
4295                    .unwrap_or_default(),
4296                note = fact.note,
4297            ),
4298        ));
4299    }
4300}
4301
4302/// Take one measure off the plan.
4303fn measure(
4304    m: &Measure,
4305    plan: &SitePlanContent,
4306    by_node: &BTreeMap<&str, &Placed<'_>>,
4307    datums: &BTreeMap<&str, i64>,
4308    i: usize,
4309) -> Measured {
4310    let missing_node = |node: &NodeId| {
4311        Measured::Unresolved(Diagnostic::error(
4312            DW_PLAN_AGREEMENT,
4313            "site-plan",
4314            format!("/content/identities/{i}/measure"),
4315            format!(
4316                "this identity measures `{node}`, which this plan embeds no box for. A measure \
4317                 is taken off the geometry, so it can only name a place the plan actually put \
4318                 somewhere."
4319            ),
4320        ))
4321    };
4322    match m {
4323        Measure::RegionExtent { axis } => {
4324            Measured::Value(f64::from(plan.region.extent[axis.index()].get()))
4325        }
4326        Measure::BoxExtent { node, axis } => match by_node.get(node.0.as_str()) {
4327            Some(p) => Measured::Value(f64::from(p.plan.extent[axis.index()].get())),
4328            None => missing_node(node),
4329        },
4330        Measure::BoxHeight { node } => match by_node.get(node.0.as_str()) {
4331            Some(p) => match p.clearance {
4332                Some(c) => Measured::Value(f64::from(c)),
4333                None => Measured::Unresolved(Diagnostic::error(
4334                    DW_PLAN_AGREEMENT,
4335                    "site-plan",
4336                    format!("/content/identities/{i}/measure"),
4337                    format!(
4338                        "this identity measures the height of `{node}`, which is sky-open and \
4339                         whose size class did not resolve — so the plan states no headroom for \
4340                         it at all. Fix the class name the layout graph declares (`DW0812` names \
4341                         it) and the height becomes the class's own minimum."
4342                    ),
4343                )),
4344            },
4345            None => missing_node(node),
4346        },
4347        Measure::DistanceXz { from, to } => {
4348            let (Some(a), Some(b)) = (by_node.get(from.0.as_str()), by_node.get(to.0.as_str()))
4349            else {
4350                return missing_node(if by_node.contains_key(from.0.as_str()) {
4351                    to
4352                } else {
4353                    from
4354                });
4355            };
4356            let (ax, az) = a.centre_xz();
4357            let (bx, bz) = b.centre_xz();
4358            Measured::Value(((bx - ax).powi(2) + (bz - az).powi(2)).sqrt())
4359        }
4360        Measure::DatumY { datum } => match datums.get(datum.0.as_str()) {
4361            Some(y) => Measured::Value(*y as f64),
4362            None => Measured::Unresolved(Diagnostic::error(
4363                crate::codes::DANGLING_REF,
4364                "site-plan",
4365                format!("/content/identities/{i}/measure"),
4366                format!(
4367                    "this identity measures `{datum}`, which this plan declares no `datums[]` \
4368                     entry for."
4369                ),
4370            )),
4371        },
4372    }
4373}
4374
4375/// What a measure is, in a refusal's own words.
4376fn describe(m: &Measure) -> String {
4377    match m {
4378        Measure::RegionExtent { axis } => {
4379            format!("the region's extent on {}", axis.as_str())
4380        }
4381        Measure::BoxExtent { node, axis } => {
4382            format!("`{node}`'s footprint on {}", axis.as_str())
4383        }
4384        Measure::BoxHeight { node } => format!("`{node}`'s headroom"),
4385        Measure::DistanceXz { from, to } => {
4386            format!("the horizontal distance from `{from}` to `{to}`")
4387        }
4388        Measure::DatumY { datum } => format!("the plane `{datum}`"),
4389    }
4390}
4391
4392impl Axis {
4393    fn index(self) -> usize {
4394        match self {
4395            Axis::X => 0,
4396            Axis::Y => 1,
4397            Axis::Z => 2,
4398        }
4399    }
4400
4401    fn as_str(self) -> &'static str {
4402        match self {
4403            Axis::X => "x",
4404            Axis::Y => "y",
4405            Axis::Z => "z",
4406        }
4407    }
4408}
4409
4410impl PlanAxis {
4411    fn index(self) -> usize {
4412        match self {
4413            PlanAxis::X => 0,
4414            PlanAxis::Z => 1,
4415        }
4416    }
4417
4418    fn as_str(self) -> &'static str {
4419        match self {
4420            PlanAxis::X => "x",
4421            PlanAxis::Z => "z",
4422        }
4423    }
4424}
4425
4426impl VolumeRole {
4427    /// The keyword a refusal prints.
4428    #[must_use]
4429    pub fn as_str(self) -> &'static str {
4430        match self {
4431            VolumeRole::Massif => "massif",
4432            VolumeRole::Ground => "ground",
4433            VolumeRole::Clearance => "clearance",
4434        }
4435    }
4436}
4437
4438/// The plan's one lighting setting, range-checked exactly as an area's is — the
4439/// same code, because it is the same object being asked the same question.
4440fn lighting(plan: &SitePlanContent, d: &mut Vec<Diagnostic>) {
4441    let Some(l) = &plan.lighting else { return };
4442    if (1..=14).contains(&l.min_light) {
4443        return;
4444    }
4445    d.push(Diagnostic::error(
4446        crate::codes::LIGHTING_RANGE,
4447        "site-plan",
4448        "/content/lighting/min_light",
4449        format!(
4450            "`lighting.min_light` = {} is out of range — set it to a value in 1..=14 (7 is the \
4451             default)",
4452            l.min_light
4453        ),
4454    ));
4455}
4456
4457/// The floor a designed opening may never be chosen below: the cells a standing
4458/// body needs to pass at all.
4459///
4460/// Not a check of its own — [`Metrics::self_check`] already holds every standard
4461/// opening over it, and a second refusal here would be this module re-asking a
4462/// question the table has already answered about itself. It is re-exported so a
4463/// reader of `DW0829` can see what the standard set is bounded by.
4464#[must_use]
4465pub fn passable_opening_cells() -> (u32, u32) {
4466    (passable_width_cells(), passable_clearance_cells())
4467}
4468
4469#[cfg(test)]
4470mod tests {
4471    use super::*;
4472
4473    /// **The run beside a pierced floor is not the host's extent**, and this is
4474    /// the arithmetic the whole one-authority repair turns on.
4475    ///
4476    /// An eight-by-eight host with a three-wide hole cut through the ceiling
4477    /// above it: the treads leave along ONE side of the hole, so what they have
4478    /// is the room on the roomier side plus the hole's own width. Centre the
4479    /// hole and that is 3 + 3 = 6, not 8 — and the courses have to carry the
4480    /// body to the pierced plane, which is one below the floor of the place
4481    /// above it.
4482    ///
4483    /// The numbers are read off the geometry by hand rather than recomputed
4484    /// here, so a change to the rule shows up as a failure rather than as two
4485    /// arithmetics agreeing.
4486    #[test]
4487    fn a_run_beside_a_pierced_floor_is_the_room_on_one_side_plus_the_hole() {
4488        // Host x 4..11, z 4..11, walk plane y 56; the hole is cut at y 63 and
4489        // spans x 7..9 — three of the eight cells, three to the low side and
4490        // two to the high.
4491        let run = stair_run(56, [4, 11, 4, 11], 1, 63, ([7, 63, 6], [9, 63, 8]))
4492            .expect("the hole is over this host and the plane is above its floor");
4493        assert_eq!(run.climb, 7, "56 up to the pierced plane at 63");
4494        assert_eq!(run.run_axis, 0, "the host is square, so the run takes x");
4495        assert_eq!(
4496            run.available, 6,
4497            "three cells of room on the low side, plus the hole's three"
4498        );
4499        assert_eq!(
4500            run.start, 9,
4501            "course 0 stands under the far edge of the hole"
4502        );
4503        assert_eq!(run.step, -1, "and the run walks back into the room");
4504    }
4505
4506    /// The same host, the same hole, and the check and the derivation asking the
4507    /// same question: seven courses do not fit six cells, so no standard pitch
4508    /// does either. Measured against the host's extent it would — which is the
4509    /// green a plan used to reach before building nothing.
4510    #[test]
4511    fn no_standard_pitch_fits_a_climb_of_seven_in_six_cells_of_run() {
4512        let table = Metrics::table();
4513        let mut reads = Reads::default();
4514        assert!(gentlest_pitch(&table, &mut reads, 7, 6).is_none());
4515        assert!(gentlest_pitch(&table, &mut reads, 7, 7).is_some());
4516        assert_eq!(tightest_pitch(&table, &mut reads, 7), Some(("stair", 7)));
4517    }
4518
4519    /// Across a vertical face the run walks the whole footprint and the climb is
4520    /// the seam's own sill — the body stands at the sill and steps through.
4521    #[test]
4522    fn a_run_across_a_vertical_face_is_the_whole_footprint() {
4523        // Host x 4..11, z 4..11, walk plane y 64; the wall is at x 12 and the
4524        // opening's low corner is at y 72.
4525        let run = stair_run(64, [4, 11, 4, 11], 0, 12, ([12, 72, 6], [12, 74, 8]))
4526            .expect("the wall is beyond the host, so the run walks back from it");
4527        assert_eq!(run.climb, 8, "64 up to the sill at 72");
4528        assert_eq!(run.run_axis, 0);
4529        assert_eq!(run.available, 8, "the host's whole extent on x");
4530        assert_eq!(run.start, 11, "course 0 stands against the wall");
4531        assert_eq!(run.step, -1);
4532    }
4533}