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