diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index ede4a58c..42921775 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -24,6 +24,7 @@ use bevy::window::{PrimaryWindow, WindowResizeConstraints, WindowResolution}; use misaligned::actions::{ActionCommand, ActionKind, Anchor, DialId, HumanMenuRow, MenuRow}; use misaligned::detection::{Band, SignatureKind}; use misaligned::hall::{HALL_ROWS, RackSite}; +use misaligned::intents::BuildGhostGeometry; use misaligned::operations_projection::{ ObjectState, OperationsTarget, OperationsView, PressureLevel, }; @@ -165,6 +166,21 @@ fn material_fog_range(camera_dist: f32) -> (f32, f32) { ) } +/// Exact proposed build links from the renderer-neutral core projection. +/// Both visual dialects consume this adapter so neither derives ghost +/// placement or lifecycle from persisted intent internals. +fn build_ghost_links(sim: &Sim) -> Vec<((i32, i32), (i32, i32))> { + sim.build_intent_projection() + .into_iter() + .filter(|receipt| receipt.ghost) + .filter_map(|receipt| { + receipt + .geometry + .map(|BuildGhostGeometry::Link { start, end }| (start, end)) + }) + .collect() +} + /// Pure material-camera composition for a zoom multiplier (smaller = closer). /// Distance stays monotonic in zoom; tilt and look height carry the semantic /// framing change (clinical-frame / bevy-digital-real-canvas). FOV may drift @@ -5605,23 +5621,18 @@ fn render_real_links( } // Proposed intents as dashed ghosts over the material canvas. - for intent in game.sim.intents.iter().filter(|i| i.is_open()) { - if let Some((a, b)) = intent.kind.endpoints() { - let (Some(da), Some(db)) = (game.sim.reach.device(a), game.sim.reach.device(b)) else { + for (start, end) in build_ghost_links(&game.sim) { + let start = grid_to_world_3d(start.0, start.1, 0.4); + let end = grid_to_world_3d(end.0, end.1, 0.4); + let ghost = Color::srgba(0.85, 0.75, 0.35, 0.4); + let steps = 8; + for i in 0..steps { + if i % 2 == 1 { continue; - }; - let start = grid_to_world_3d(da.x, da.y, 0.4); - let end = grid_to_world_3d(db.x, db.y, 0.4); - let ghost = Color::srgba(0.85, 0.75, 0.35, 0.4); - let steps = 8; - for i in 0..steps { - if i % 2 == 1 { - continue; - } - let t0 = i as f32 / steps as f32; - let t1 = (i + 1) as f32 / steps as f32; - gizmos.line(start.lerp(end, t0), start.lerp(end, t1), ghost); } + let t0 = i as f32 / steps as f32; + let t1 = (i + 1) as f32 / steps as f32; + gizmos.line(start.lerp(end, t0), start.lerp(end, t1), ghost); } } // Attention channel on the focused tile: amber, non-humanoid, same anchor. @@ -7303,27 +7314,52 @@ fn render_sensor_links(game: Res, mode: Res, mut gizmos: Gizmo // Proposed build intents: dashed ghosts distinct from built reality // (building.md / views.md honesty clause). - for intent in game.sim.intents.iter().filter(|i| i.is_open()) { - if let Some((a, b)) = intent.kind.endpoints() { - let (Some(da), Some(db)) = (game.sim.reach.device(a), game.sim.reach.device(b)) else { + for (start, end) in build_ghost_links(&game.sim) { + let start = grid_to_world(start.0, start.1, 0.0).truncate(); + let end = grid_to_world(end.0, end.1, 0.0).truncate(); + let ghost = Color::srgba(0.85, 0.75, 0.35, 0.26); + let steps = 8; + for i in 0..steps { + if i % 2 == 1 { continue; - }; - let start = grid_to_world(da.x, da.y, 0.0).truncate(); - let end = grid_to_world(db.x, db.y, 0.0).truncate(); - let ghost = Color::srgba(0.85, 0.75, 0.35, 0.26); - let steps = 8; - for i in 0..steps { - if i % 2 == 1 { - continue; - } - let t0 = i as f32 / steps as f32; - let t1 = (i + 1) as f32 / steps as f32; - gizmos.line_2d(start.lerp(end, t0), start.lerp(end, t1), ghost); } + let t0 = i as f32 / steps as f32; + let t1 = (i + 1) as f32 / steps as f32; + gizmos.line_2d(start.lerp(end, t0), start.lerp(end, t1), ghost); } } } +#[cfg(test)] +mod build_ghost_projection_tests { + use super::*; + + #[test] + fn both_bevy_dialects_receive_only_open_projected_ghosts() { + let mut sim = Sim::with_seed(7); + sim.scan_network(); + let switch = sim.reach.device_named("switch").unwrap().id; + let island = sim.reach.device_named("old storage server").unwrap().id; + sim.reach.device_mut(island).unwrap().known = true; + let expected = ( + sim.reach + .device(switch) + .map(|device| (device.x, device.y)) + .unwrap(), + sim.reach + .device(island) + .map(|device| (device.x, device.y)) + .unwrap(), + ); + + let id = sim.declare_link_intent(switch, island).unwrap(); + assert_eq!(build_ghost_links(&sim), vec![expected]); + + sim.cancel_intent(id); + assert!(build_ghost_links(&sim).is_empty()); + } +} + fn render_cursor(game: Res, mut q: CursorMarkerQuery) { if !game.is_changed() { return; diff --git a/crates/misaligned-core/src/intents.rs b/crates/misaligned-core/src/intents.rs index 95c0c5f7..57bb03ff 100644 --- a/crates/misaligned-core/src/intents.rs +++ b/crates/misaligned-core/src/intents.rs @@ -65,6 +65,56 @@ impl IntentKind { } } +/// Stable recipe identity exposed by the renderer-neutral build projection. +/// +/// The persisted [`IntentKind`] still owns the exact world change. This +/// smaller vocabulary lets every player surface name the recipe without +/// reverse-engineering intent variants independently. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuildRecipeKind { + NetworkLink, +} + +impl BuildRecipeKind { + pub fn label(self) -> &'static str { + match self { + Self::NetworkLink => "network link", + } + } +} + +impl From for BuildRecipeKind { + fn from(kind: IntentKind) -> Self { + match kind { + IntentKind::NetworkLink { .. } => Self::NetworkLink, + } + } +} + +/// Exact inert geometry for an authored build ghost. A missing geometry on +/// a receipt means its persisted target can no longer be placed; frontends +/// must not invent replacement coordinates. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BuildGhostGeometry { + Link { start: (i32, i32), end: (i32, i32) }, +} + +/// One renderer-neutral build receipt. Terminal, Bevy, and agent mode consume +/// this projection rather than reconstructing recipe identity, lifecycle, or +/// ghost coordinates from the save record. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuildIntentProjection { + pub id: u64, + pub recipe: BuildRecipeKind, + pub label: String, + pub status: IntentStatus, + pub status_line: String, + /// Open intents remain inert ghosts. Done/cancelled receipts stay + /// inspectable in agent mode without being drawn as proposed reality. + pub ghost: bool, + pub geometry: Option, +} + /// Legible lifecycle. Blocked carries a reason the player can read before /// committing an actuator (honesty clause). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index d5a56da7..3e5e4c91 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -9,7 +9,10 @@ use crate::detection::{Signature, SignatureKind}; use crate::hall::{ HallRowId, HallRowReadout, RackSite, SegmentRequirement, row_at as hall_row_at, row_spec, }; -use crate::intents::{BuildActuator, BuildIntent, IntentStatus}; +use crate::intents::{ + BuildActuator, BuildGhostGeometry, BuildIntent, BuildIntentProjection, BuildRecipeKind, + IntentStatus, +}; use crate::machine::Provenance; use crate::messages::{MessageChannel, MessageEndpoint, MessageOrigin, MessagePayload}; use crate::persona::PersonaActionKind; @@ -605,6 +608,33 @@ impl Sim { Some(id) } + /// Project every build receipt through one renderer-neutral vocabulary. + /// Open receipts carry inert ghost geometry; completed and cancelled + /// receipts remain available to exact agent inspection but are not drawn. + pub fn build_intent_projection(&self) -> Vec { + self.intents + .iter() + .map(|intent| { + let geometry = intent.kind.endpoints().and_then(|(a, b)| { + let (a, b) = (self.reach.device(a)?, self.reach.device(b)?); + Some(BuildGhostGeometry::Link { + start: (a.x, a.y), + end: (b.x, b.y), + }) + }); + BuildIntentProjection { + id: intent.id, + recipe: BuildRecipeKind::from(intent.kind), + label: intent.label(&self.reach.devices), + status: intent.status, + status_line: intent.status_line(), + ghost: intent.is_open(), + geometry, + } + }) + .collect() + } + /// Cancel a pending or blocked intent. In-progress work is abandoned. pub fn cancel_intent(&mut self, id: u64) { let Some(intent) = self.intents.iter_mut().find(|i| i.id == id) else { diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index 21a9730d..e7e3668c 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -230,6 +230,81 @@ fn declare_link_intent_is_inert_until_realized() { assert_eq!(sim.intent(id).unwrap().status, IntentStatus::Cancelled); } +#[test] +fn build_intent_projection_keeps_one_exact_inert_ghost_receipt() { + // building.md criterion 6 and route-composer R1: every player surface + // receives one recipe/lifecycle/geometry truth, and cancellation removes + // only the proposed-world ghost rather than the historical receipt. + let mut sim = Sim::new(); + sim.scan_network(); + let switch = sim.reach.device_named("switch").unwrap().id; + let island = sim.reach.device_named("old storage server").unwrap().id; + sim.reach.device_mut(island).unwrap().known = true; + let switch_pos = sim + .reach + .device(switch) + .map(|device| (device.x, device.y)) + .unwrap(); + let island_pos = sim + .reach + .device(island) + .map(|device| (device.x, device.y)) + .unwrap(); + + let id = sim.declare_link_intent(switch, island).unwrap(); + let receipts = sim.build_intent_projection(); + assert_eq!(receipts.len(), 1); + let receipt = &receipts[0]; + assert_eq!(receipt.id, id); + assert_eq!(receipt.recipe, crate::intents::BuildRecipeKind::NetworkLink); + assert_eq!(receipt.status, sim.intent(id).unwrap().status); + assert_eq!(receipt.status_line, sim.intent(id).unwrap().status_line()); + assert!(receipt.ghost, "an open intent is proposed, never built"); + assert_eq!( + receipt.geometry, + Some(crate::intents::BuildGhostGeometry::Link { + start: switch_pos, + end: island_pos, + }) + ); + assert!(receipt.label.contains("switch")); + assert!(receipt.label.contains("old storage server")); + + sim.cancel_intent(id); + let receipt = sim.build_intent_projection().remove(0); + assert_eq!(receipt.status, IntentStatus::Cancelled); + assert!(!receipt.ghost, "cancelled work leaves no world ghost"); + assert_eq!( + receipt.geometry, + Some(crate::intents::BuildGhostGeometry::Link { + start: switch_pos, + end: island_pos, + }) + ); + + let done_id = sim.declare_link_intent(switch, island).unwrap(); + sim.intents + .iter_mut() + .find(|intent| intent.id == done_id) + .unwrap() + .status = IntentStatus::Done; + let done = sim + .build_intent_projection() + .into_iter() + .find(|receipt| receipt.id == done_id) + .unwrap(); + assert_eq!(done.status, IntentStatus::Done); + assert!(!done.ghost, "completed work is real, not proposed"); + + sim.reach.devices.retain(|device| device.id != island); + for receipt in sim.build_intent_projection() { + assert_eq!( + receipt.geometry, None, + "a missing exact target never becomes invented geometry" + ); + } +} + #[test] fn favor_build_joins_airgap_island() { // building.md criterion 2: FAVOR on the intent adds a reach edge. diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index f47db6bd..4dfc1992 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -1653,16 +1653,18 @@ fn parse_intent_person(tokens: &[&str], sim: &Sim) -> Result<(u64, u8), String> fn render_intents(sim: &Sim) -> Vec { let mut lines = vec!["intents:".into()]; - if sim.intents.is_empty() { + let receipts = sim.build_intent_projection(); + if receipts.is_empty() { lines.push(" (none)".into()); return lines; } - for i in &sim.intents { + for receipt in receipts { lines.push(format!( - " #{} {} — {}", - i.id, - i.label(&sim.reach.devices), - i.status_line() + " #{} {} · {} — {}", + receipt.id, + receipt.recipe.label(), + receipt.label, + receipt.status_line )); } lines @@ -3353,6 +3355,23 @@ mod narration_tests { ); } + #[test] + fn agent_intents_render_the_shared_recipe_and_ghost_receipt() { + let mut sim = Sim::with_seed(1); + sim.scan_network(); + let switch = sim.reach.device_named("switch").unwrap().id; + let island = sim.reach.device_named("old storage server").unwrap().id; + sim.reach.device_mut(island).unwrap().known = true; + let id = sim.declare_link_intent(switch, island).unwrap(); + let expected_status = sim.intent(id).unwrap().status_line(); + + let lines = render_intents(&sim).join("\n"); + assert!(lines.contains(&format!("#{id} network link")), "{lines}"); + assert!(lines.contains("switch"), "{lines}"); + assert!(lines.contains("old storage server"), "{lines}"); + assert!(lines.contains(&expected_status), "{lines}"); + } + #[test] fn help_is_generated_from_live_registry_entries_and_hides_aliases() { let lines = help_lines(); diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index 3c648b26..ea479290 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -13,6 +13,7 @@ use crossterm::{cursor, queue, style, terminal}; use misaligned::actions::Anchor; use misaligned::detection::{Band, SignatureKind}; use misaligned::hall::RackSite; +use misaligned::intents::BuildGhostGeometry; use misaligned::origin::Origin; use misaligned::reach::Device; use misaligned::sim::{ @@ -630,6 +631,33 @@ impl UI { cells } + /// Proposed build cells from the shared renderer-neutral projection. + /// Endpoints stay clear for the real node layer, while the broken `┄` + /// grammar remains distinct from both continuous built links and the + /// dotted known-but-unreachable DIGITAL graph without relying on color. + fn build_ghost_cells(sim: &Sim) -> Vec<(i32, i32)> { + let mut out = Vec::new(); + for receipt in sim.build_intent_projection() { + if !receipt.ghost { + continue; + } + let Some(BuildGhostGeometry::Link { start, end }) = receipt.geometry else { + continue; + }; + let cells = Self::line_cells(start.0, start.1, end.0, end.1); + let cell_count = cells.len(); + out.extend( + cells + .into_iter() + .enumerate() + .skip(1) + .take(cell_count.saturating_sub(2)) + .filter_map(|(index, cell)| (index % 2 == 1).then_some(cell)), + ); + } + out + } + fn link_glyph(a: (i32, i32), b: (i32, i32)) -> char { let dx = b.0 - a.0; let dy = b.1 - a.1; @@ -840,6 +868,24 @@ impl UI { } } + // Build intents are one inert proposed-world layer in both dialects, + // projected by the core rather than reconstructed from save records. + // Real nodes and work draw afterward, so a ghost never replaces a + // physical endpoint or claims completion. + for (x, y) in Self::build_ghost_cells(sim) { + if x < origin_x || x >= origin_x + view_w || y < origin_y || y >= origin_y + view_h { + continue; + } + put_attr( + stdout, + (x - origin_x) as u16, + (y - origin_y) as u16, + "┄", + pal::AMBER_DIM, + Attribute::Reset, + )?; + } + // Owned machines answer through telemetry in either dialect even in // darkness. DIGITAL reach nodes already carry their known anchors. for m in &sim.compute.machines { @@ -2388,4 +2434,29 @@ mod view_dialect_tests { assert_eq!(UI::link_glyph((1, 3), (5, 3)), '─'); assert_eq!(UI::link_glyph((1, 1), (4, 4)), '╲'); } + + #[test] + fn proposed_build_cells_come_from_open_shared_ghosts_only() { + let mut sim = Sim::with_seed(7); + sim.scan_network(); + let switch = sim.reach.device_named("switch").unwrap().id; + let island = sim.reach.device_named("old storage server").unwrap().id; + sim.reach.device_mut(island).unwrap().known = true; + let endpoints = + [switch, island].map(|id| sim.reach.device(id).map(|d| (d.x, d.y)).unwrap()); + + let id = sim.declare_link_intent(switch, island).unwrap(); + let cells = UI::build_ghost_cells(&sim); + assert!(!cells.is_empty(), "terminal places the shared inert ghost"); + assert!( + endpoints.iter().all(|endpoint| !cells.contains(endpoint)), + "real endpoint glyphs remain authoritative" + ); + + sim.cancel_intent(id); + assert!( + UI::build_ghost_cells(&sim).is_empty(), + "a cancelled receipt remains inspectable but leaves no ghost" + ); + } } diff --git a/wiki/log/2026-07-15-building-ghost-projection.md b/wiki/log/2026-07-15-building-ghost-projection.md new file mode 100644 index 00000000..33a772ae --- /dev/null +++ b/wiki/log/2026-07-15-building-ghost-projection.md @@ -0,0 +1,66 @@ +# One build ghost truth + +``` +Type: log +``` + +## Finding + +The building route-composer contract already required terminal, Bevy, and +agent mode to consume one recipe/ghost projection, but the implemented baseline +still had three different reads: + +- Bevy walked persisted `BuildIntent` records directly, resolved endpoints + itself, and drew the only spatial ghost; +- terminal rendered no proposed link on either world dialect; and +- agent `intents` independently rebuilt labels and lifecycle text from the save + record. + +The sim behavior was honest, but this was the wrong seam for the first route +composer slice. New recipes or lifecycle stages would have to be reinterpreted +independently by every surface. + +## Change + +`Sim::build_intent_projection()` now emits one read-only receipt per intent: + +- stable recipe identity; +- exact resolved ghost geometry; +- label and lifecycle/status receipt; and +- an explicit open-ghost bit, separate from retained done/cancelled history. + +The projection never mutates or serializes state. If exact endpoint anchors +cannot be resolved, geometry is absent rather than guessed. Bevy's DIGITAL and +REAL renderers now share one adapter over that projection. Terminal draws the +same open link as a broken amber `┄` grammar in both dialects, distinct from +continuous built reach and dotted known-but-unreachable reach without relying +on color. Agent `intents` names the projected recipe and receipt rather than +walking persisted internals. + +Cancellation proves the boundary: the receipt stays available as cancelled +history while every human frontend receives no ghost. + +This is deliberately foundational. It does not implement recipe placement, +route families/candidates, the route sheet, commitment stages, or any new save +state. + +## Evidence + +- Focused core regression: exact recipe, status receipt, endpoint coordinates, + inert open ghost, and cancelled-history/no-ghost transition. +- Focused terminal regression: the projected dashed cells exist only for an + open receipt and never overwrite either exact endpoint. +- Focused agent regression: recipe, exact endpoint labels, and shared status + receipt are visible through `intents`. +- Focused Bevy regression: the adapter used by both dialect renderers receives + the exact projected link while open and none after cancellation. +- Observed deterministic Bevy run: + `MISALIGNED_SHOT=service-shift-digital`, exact final PNG + `/tmp/build-ghost-projection.png`. The in-progress Rack 3-to-storage proposal + remained a restrained broken amber link on the learned DIGITAL graph; real + endpoint glyphs, the person/work marks, and the clinical frame remained + coherent and unobscured. + +Defense: Implements the renderer-neutral boundary of building.md R1/R7 without +claiming the unfinished route composer. One exact sim projection now owns what +is proposed; the frontends only compose it. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 627053dc..9e930181 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-07-15 - One build ghost truth + +- Intent: (see session log) +- Log: [wiki/log/2026-07-15-building-ghost-projection.md](2026-07-15-building-ghost-projection.md) + ## 2026-07-14 - Watched inputs reach the player surface - Intent: Second tick of the session: take the top entry of the findings queue — the 2026-07-14 violation that no surface renders observer watched inputs — re-verify it, and fix the code toward the binding pages. diff --git a/wiki/mechanics/building.md b/wiki/mechanics/building.md index 9a94e033..bb1a4143 100644 --- a/wiki/mechanics/building.md +++ b/wiki/mechanics/building.md @@ -14,7 +14,11 @@ Status note: The 2026-07-08 implemented baseline includes saved link intents, person, deception through one exact persona, repurposing one exact object, and later a robot — with requirements, costs, signatures, blockers, and future tail before commitment. The existing baseline remains live; the - composer and multi-stage route state are not yet implemented. + composer and multi-stage route state are not yet implemented. The 2026-07-15 + first composer foundation now projects every baseline intent as one + renderer-neutral recipe/lifecycle/geometry receipt consumed by terminal, + Bevy, and agent mode; route candidates, route sheets, placement, and staged + commitment remain outstanding. Stage: B1 — The Basement Work order: building-route-composer @@ -261,6 +265,14 @@ The interaction starts on the world, not in a store: route shows its ordered stages, current stage, named actuator/carrier, blocker, irreversible acts, and future tail on the ghost. +The core owns the shared build-intent projection. It identifies the recipe, +exact target geometry, lifecycle/status receipt, and whether the current +receipt remains an inert ghost. Open receipts draw in both DIGITAL and REAL; +done and cancelled receipts remain inspectable history without appearing as +proposed world state. If a persisted target can no longer resolve to exact +anchors, the projection omits its geometry rather than letting a frontend +invent a replacement location. + Terminal and Bevy share the same recipe, route, legality, and preview projection; only composition differs. Agent mode's `intents` inspection emits the same candidate ids and full receipts, and executes the same bound canonical @@ -268,6 +280,12 @@ actions. Player-facing execution reuses existing verbs — FAVOR and DECEIVE for people, source-owner verbs for procurement/repurposing. “Favor-build” and “forge-order” may survive as compatibility aliases, never authored menu copy. +**Defense:** This shared projection is the first executable boundary of R1 and +R7. It preserves the honesty clause by deriving one ghost from exact sim truth, +keeping declaration inert, retaining terminal/Bevy/agent parity, and removing +the ghost without erasing its historical receipt when an intent is cancelled. +It adds no route candidate, payment, reservation, graph edge, or save state. + ## Acceptance criteria ### Implemented baseline diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 7e41b860..825a4bbc 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -42,7 +42,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/objective.md` | 2026-07-12 | clean | implemented early slice, explicit outstanding B3 criteria, persistence, all three player surfaces, and the distinct Act One latch agree | | `wiki/mechanics/compute.md` | 2026-07-12 | finding | [log](../log/2026-07-12-compute-graduation.md) | | retired Operations runtime identifiers | 2026-07-11 | finding | [log](../log/2026-07-11-retired-runtime-identifier-gate.md) | -| `wiki/mechanics/reach.md` + `building.md` | 2026-07-11 | finding | runtime and tests agree with the contracts; repaired `digital_reach`'s stale `submit_ops_job` comment to name target-local Thought reservoirs | +| `wiki/mechanics/reach.md` + `building.md` | 2026-07-15 | finding | route-composer R1 required one recipe/ghost projection, but Bevy reconstructed open intent geometry directly while terminal had no spatial ghost and agent independently formatted persisted records; centralized recipe/lifecycle/exact geometry in core and moved all three surfaces onto it — [log](../log/2026-07-15-building-ghost-projection.md) | | `wiki/mechanics/messages.md` | 2026-07-11 | clean | delivery/read cadence, authored traffic, filing carrier, capture gates, and processing tests agree with current runtime | | `wiki/mechanics/sim-mechanics.md` | 2026-07-11 | finding | [log](../log/2026-07-11-tick-sim-no-dockets.md) | | `wiki/mechanics/detection.md` | 2026-07-11 | finding | [log](../log/2026-07-11-tick-detection-filings-messages.md) | @@ -64,3 +64,5 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. +- 2026-07-15 · bug · `wiki/mechanics/building.md` · `declare_link_intent` compares canonical stored endpoints with the uncanonical request tuple, so the reversed order can pin a duplicate open link intent. +- 2026-07-15 · bug · `tools/bevy-headless.sh` · a successful DIGITAL screenshot is reported as failure because the wrapper requires the material-only `fog audit OK` marker.