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