From 1a4cd11fb4ed58320445a5f4484d486ada65ae45 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Mon, 27 Jul 2026 02:58:05 -0700 Subject: [PATCH] Populate B1 sensors by topology. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn sensor access into a real curation choice while preserving the authored first-sight boundary and leaving unimplemented passage telemetry explicit. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- crates/misaligned-core/src/map.rs | 11 + crates/misaligned-core/src/prefab.rs | 75 ++ crates/misaligned-core/src/reach.rs | 969 ++++++++++++++---- crates/misaligned-core/src/save.rs | 2 +- .../misaligned-core/src/sim/tests/carrier.rs | 15 + .../src/sim/tests/reach_build.rs | 36 + crates/misaligned-core/src/ui_projection.rs | 82 +- wiki/engineering/current-build.md | 4 +- ...2026-07-27-sensor-population-foundation.md | 59 ++ wiki/log/DEVLOG.md | 5 + wiki/mechanics/sensor-network.md | 63 +- wiki/process/ROADMAP.md | 2 +- wiki/process/specs.md | 2 +- wiki/world/places/basement-map.md | 11 + 14 files changed, 1131 insertions(+), 205 deletions(-) create mode 100644 wiki/log/2026-07-27-sensor-population-foundation.md diff --git a/crates/misaligned-core/src/map.rs b/crates/misaligned-core/src/map.rs index 37e45938..75e82cb3 100644 --- a/crates/misaligned-core/src/map.rs +++ b/crates/misaligned-core/src/map.rs @@ -25,6 +25,17 @@ impl GameMap { map } + /// Build a GameMap from any prefab Layout, preserving its room rects. + /// Used by tests that need a non-basement floor (e.g. the toy sensor + /// floor proving the sensor population rule travels). + #[cfg(test)] + pub(crate) fn from_layout(layout: &crate::prefab::Layout) -> Self { + let flat = layout.tiles(); + let mut map = Self::from_tiles(layout.width, layout.height, flat, HashSet::new()); + map.rooms = layout.rooms.clone(); + map + } + /// Reconstruct a GameMap from saved tile data. pub fn from_tiles( width: i32, diff --git a/crates/misaligned-core/src/prefab.rs b/crates/misaligned-core/src/prefab.rs index 82b553f4..e80856c2 100644 --- a/crates/misaligned-core/src/prefab.rs +++ b/crates/misaligned-core/src/prefab.rs @@ -491,6 +491,81 @@ pub fn toy_layout() -> Layout { layout } +/// A toy floor with a few small rooms and a corridor, built to prove the +/// sensor population rule travels (sensor-network.md criterion 2). Unlike +/// `toy_layout` (which reuses basement prefabs), this floor has its own +/// authored rooms with security doors, so the rule generates room cameras, +/// corridor nodes, and badge readers against fresh topology — not the +/// basement's. +#[cfg(test)] +const TOY_SENSOR_ROOM_A: Prefab = Prefab { + name: "toy_office", + rows: &["#######", "#.....#", "#.....#", "#.....#", "##2####"], +}; + +#[cfg(test)] +const TOY_SENSOR_ROOM_B: Prefab = Prefab { + name: "toy_lab", + rows: &["#######", "#.....#", "#.....#", "#.....#", "##2####"], +}; + +#[cfg(test)] +const TOY_SENSOR_ROOM_C: Prefab = Prefab { + name: "toy_storage", + rows: &["#####", "#...#", "#...#", "#...#", "##+##"], +}; + +/// Build the toy sensor floor: three rooms connected by a corridor, with +/// two T2 security doors and one plain door. The sensor population rule +/// generates room cameras, corridor nodes, and badge readers against this +/// fresh topology, proving the rule travels (sensor-network.md criterion 2). +#[cfg(test)] +pub(crate) fn toy_sensor_floor() -> Layout { + let prefabs = vec![TOY_SENSOR_ROOM_A, TOY_SENSOR_ROOM_B, TOY_SENSOR_ROOM_C]; + let placements = vec![ + Placement { + prefab: 0, + x: 2, + y: 1, + }, // toy_office (T2 door at (4,5)) + Placement { + prefab: 1, + x: 12, + y: 1, + }, // toy_lab (T2 door at (14,5)) + Placement { + prefab: 2, + x: 22, + y: 1, + }, // toy_storage (plain door at (24,5)) + ]; + let rooms = rooms_of(&prefabs, &placements); + let mut layout = Layout { + width: 30, + height: 10, + prefabs, + placements, + rooms, + composed: Vec::new(), + cached: None, + }; + let mut tiles = layout.stamp(); + // Carve a corridor gallery at y=6 connecting all three room doors. + carve_corridors( + &mut tiles, + layout.width, + &[ + ((2, 6), (27, 6)), // main gallery at y=6 + ((4, 5), (4, 6)), // office T2 door to gallery + ((14, 5), (14, 6)), // lab T2 door to gallery + ((24, 5), (24, 6)), // storage door to gallery + ], + ); + layout.placements.clear(); + layout.cached = Some(tiles); + layout +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/misaligned-core/src/reach.rs b/crates/misaligned-core/src/reach.rs index b9dc7a16..31efb392 100644 --- a/crates/misaligned-core/src/reach.rs +++ b/crates/misaligned-core/src/reach.rs @@ -234,6 +234,220 @@ pub enum ReachBlock { AirGap, } +// ─── Sensor population rule (sensor-network.md criteria 1-2) ───────────── + +/// Spacing between corridor camera nodes along a service gallery +/// (sensor-network.md: "one `camera_node` roughly every eight tiles"). +const CORRIDOR_NODE_SPACING: i32 = 8; + +/// Generate sensing devices by rule from the map's room and corridor topology +/// (sensor-network.md criteria 1-2). +/// +/// The rule is binding; the exact total is authoring detail. This function +/// populates: +/// +/// | Class | Rule | +/// |---|---| +/// | Room cameras | one per enclosed room that does not already have a seeing device | +/// | Corridor nodes | one every ~8 tiles along each horizontal gallery of corridor floor | +/// | Hall aisle cameras | one per cold aisle in the data hall, plus the hall's door approaches | +/// | Badge readers | one per security door that does not already have a badge controller | +/// +/// Segment assignment: every rule-generated ambient camera and badge +/// reader belongs to the facility security segment (1), not the opening +/// subnet. This preserves the authored opening: after SCAN but before +/// bridging, no new ambient seeing sensor is reachable, so the +/// environmental-monitor Eyes beat remains the first obtainable sight. +/// +/// The rule travels: any `GameMap` with rooms gets sensors by the same logic, +/// so a later floor inherits coverage without a new authored inventory. +/// Existing devices (created by the infrastructure author) are recognized +/// and never duplicated: a room that already has a camera tile keeps it, and +/// a security door that already has a badge controller keeps it. +fn populate_sensors(map: &GameMap, devices: &mut Vec, next_id: &mut u32) { + // ── 1. Room cameras: one per enclosed room ──────────────────────────── + for room in &map.rooms { + // Skip rooms whose name suggests they are corridors or crawlspace + // (those get corridor nodes instead). + if room.name == "corridor" || room.name == "crawlspace" { + continue; + } + // Skip if a seeing device already covers this room (the env camera, + // dock camera, stairwell camera, or any previously-generated room + // camera). This preserves existing tile-authored cameras. + let has_camera = devices.iter().any(|d| room.contains(d.x, d.y) && d.sees); + if has_camera { + continue; + } + // Place the room camera at the room center. + let (cx, cy) = room.center(); + let name = format!("{} camera", room.name); + devices.push(make_sensor_device( + *next_id, &name, cx, cy, true, // sees + )); + *next_id += 1; + } + + // ── 2. Badge readers: one per security door ────────────────────────── + // A reader is a sensing-class network node, not decoration. Passage + // telemetry is not implemented in this slice, so sensor-network criterion + // 1 remains partial rather than claiming that the node already witnesses. + for y in 0..map.height { + for x in 0..map.width { + let tile = map.get_tile(x, y); + if tile.is_door() && tile.security_level() > 0 { + // Skip if a badge controller already exists at this position. + let has_badge = devices + .iter() + .any(|d| d.x == x && d.y == y && d.name.contains("badge")); + if has_badge { + continue; + } + let name = format!("badge reader (T{} at {},{})", tile.security_level(), x, y); + devices.push(make_sensor_device( + *next_id, &name, x, y, false, // badge readers don't see tiles + )); + *next_id += 1; + } + } + } + + // ── 3. Corridor camera nodes: one every ~8 tiles along galleries ────── + // Find horizontal runs of corridor floor (walkable tiles not in any room) + // and place camera nodes at regular intervals. This covers both service + // galleries (north and south) without hard-coding their coordinates. + let corridor_cameras = find_corridor_camera_positions(map); + for (x, y) in corridor_cameras { + // Skip if a device already sits at this position. + let occupied = devices.iter().any(|d| d.x == x && d.y == y); + if occupied { + continue; + } + let name = format!("corridor camera ({},{})", x, y); + devices.push(make_sensor_device(*next_id, &name, x, y, true)); + *next_id += 1; + } + + // ── 4. Hall aisle cameras: one per cold aisle + door approaches ─────── + // The data hall's cold aisles are the walkable rows between rack banks. + // Place one camera per cold aisle, plus one at each hall door approach. + let hall = map.room_named("server_room"); + if let Some(hall_room) = hall { + for &aisle_y in &crate::hall::HALL_COLD_AISLES { + // Only place if the aisle y falls within the hall room's y range. + if !(hall_room.y..hall_room.y + hall_room.h).contains(&aisle_y) { + continue; + } + // Place at the center of the hall's width along the aisle. + let cx = hall_room.x + hall_room.w / 2; + // Skip if a device already sits here. + let occupied = devices.iter().any(|d| d.x == cx && d.y == aisle_y); + if occupied { + continue; + } + let name = format!("aisle camera ({},{})", cx, aisle_y); + devices.push(make_sensor_device(*next_id, &name, cx, aisle_y, true)); + *next_id += 1; + } + // Hall door approaches: cameras at the three T2 doors. + for y in hall_room.y..hall_room.y + hall_room.h { + for x in hall_room.x..hall_room.x + hall_room.w { + if map.get_tile(x, y) == TileType::SecurityDoor2 { + // Place a camera just outside the door (one tile toward + // the hall interior). + let (ix, iy) = if hall_room.contains(x, y + 1) { + (x, y + 1) + } else if hall_room.contains(x, y - 1) { + (x, y - 1) + } else { + (x, y) + }; + let occupied = devices.iter().any(|d| d.x == ix && d.y == iy); + if occupied { + continue; + } + let name = format!("hall door camera ({},{})", ix, iy); + devices.push(make_sensor_device(*next_id, &name, ix, iy, true)); + *next_id += 1; + } + } + } + } +} + +/// Create one rule-generated facility sensor on the security segment. +fn make_sensor_device(id: u32, name: &str, x: i32, y: i32, sees: bool) -> Device { + let feeds = if sees { + vec![Feed { + who: Party::Facility, + sight: sees, + hearing: false, + }] + } else { + Vec::new() + }; + Device { + id, + name: name.into(), + x, + y, + segment: 1, + owner: Party::Facility, + controller: Party::Facility, + sees, + hears: false, + camera_dormant: false, + radius: 4, + message_channels: Vec::new(), + accounting_carrier: false, + people_interface: false, + interface_wear: 0, + known: false, + is_switch: false, + feeds, + } +} + +/// Find positions for corridor camera nodes: one every `CORRIDOR_NODE_SPACING` +/// tiles along each horizontal run of corridor floor (walkable tiles not in +/// any room). This covers both service galleries without hard-coding +/// coordinates, so the rule travels to any floor. +fn find_corridor_camera_positions(map: &GameMap) -> Vec<(i32, i32)> { + let mut positions = Vec::new(); + for y in 0..map.height { + // Collect the horizontal run of corridor floor tiles on this row. + let mut run: Vec = Vec::new(); + for x in 0..map.width { + let tile = map.get_tile(x, y); + let in_room = map.room_at(x, y).is_some(); + if tile.is_walkable() && !in_room && tile != TileType::Entry { + run.push(x); + } else { + // Flush the current run. + if run.len() >= CORRIDOR_NODE_SPACING as usize { + place_corridor_cameras(&run, y, &mut positions); + } + run.clear(); + } + } + if run.len() >= CORRIDOR_NODE_SPACING as usize { + place_corridor_cameras(&run, y, &mut positions); + } + } + positions +} + +/// Place camera nodes at regular intervals along a corridor run. +fn place_corridor_cameras(run: &[i32], y: i32, out: &mut Vec<(i32, i32)>) { + let len = run.len() as i32; + let mut offset = CORRIDOR_NODE_SPACING / 2; // start mid-first-segment + while offset < len { + let x = run[offset as usize]; + out.push((x, y)); + offset += CORRIDOR_NODE_SPACING; + } +} + /// The device graph plus the player's standing over it: which segments they /// have bridged and which devices they control or subscribe to. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -252,10 +466,31 @@ impl ReachNet { /// the seed of the partial subnet map); segment 1 is the security /// segment behind the switch. The old storage server is an air-gapped /// island: known shape, no link, untouchable until one is built. + /// + /// Sensor devices are populated by rule (sensor-network.md criteria 1-2): + /// the [`populate_sensors`] function analyzes the map's room and corridor + /// topology and generates sensing devices — room cameras, corridor + /// nodes, hall aisle cameras, and badge readers — so the same rule + /// travels to a later floor without a new authored inventory. pub fn basement(map: &GameMap) -> Self { + Self::from_map(map, true) + } + + /// Build a device graph from any map using the sensor population rule + /// (sensor-network.md criterion 2: the rule travels). When + /// `with_infrastructure` is true, the basement's story-specific + /// infrastructure devices (Rack 3, switch, badge controller, storage + /// server, facility meters, people-facing displays) are authored first; + /// the sensor populator then fills in rule-generated sensors for every + /// room and corridor. When false (toy floor), only the sensor populator + /// runs — proving the rule is self-contained. + fn from_map(map: &GameMap, with_infrastructure: bool) -> Self { let mut devices = Vec::new(); - let mut id = 0u32; - let mut mk = |name: &str, + let mut next_id = 0u32; + + if with_infrastructure { + // ── Infrastructure devices (basement-specific) ───────────────── + let mk = |name: &str, (x, y): (i32, i32), segment: u32, owner: Party, @@ -263,143 +498,181 @@ impl ReachNet { hears: bool, camera_dormant: bool, known: bool, - is_switch: bool| { - let controller = owner; - let feeds = if sees || hears { - vec![Feed { - who: owner, - sight: sees, - hearing: hears, - }] - } else { - Vec::new() - }; - let d = Device { - id, - name: name.into(), - x, - y, - segment, - owner, - controller, - sees, - hears, - camera_dormant, - radius: 4, - message_channels: Vec::new(), - accounting_carrier: false, - people_interface: false, - interface_wear: 0, - known, - is_switch, - feeds, + is_switch: bool, + id: &mut u32| { + let controller = owner; + let feeds = if sees || hears { + vec![Feed { + who: owner, + sight: sees, + hearing: hears, + }] + } else { + Vec::new() + }; + let d = Device { + id: *id, + name: name.into(), + x, + y, + segment, + owner, + controller, + sees, + hears, + camera_dormant, + radius: 4, + message_channels: Vec::new(), + accounting_carrier: false, + people_interface: false, + interface_wear: 0, + known, + is_switch, + feeds, + }; + *id += 1; + d }; - id += 1; - d - }; - let core = map.core_pos().unwrap_or((24, 15)); - let at = |t: TileType| map.tiles_of_type(t).first().copied(); + let core = map.core_pos().unwrap_or((24, 15)); + let at = |t: TileType| map.tiles_of_type(t).first().copied(); - // Seed knowledge = the basement subnet segment (reach.md: the - // opening state's partial map). Security devices exist but are - // unknown until scanned; the storage server is unknown and unwired. - devices.push(mk( - "Rack 3", - core, - 0, - Party::Player, - false, - false, - false, - true, - false, - )); - if let Some(p) = at(TileType::Switch) { + // Seed knowledge = the basement subnet segment (reach.md: the + // opening state's partial map). Security devices exist but are + // unknown until scanned; the storage server is unknown and unwired. devices.push(mk( - "switch", - p, + "Rack 3", + core, 0, - Party::Facility, + Party::Player, false, false, false, true, - true, - )); - } - if let Some(p) = at(TileType::EnvCamera) { - devices.push(mk( - "environmental monitor", - p, - 0, - Party::Facility, - true, - true, - true, // camera dormant until the Eyes beat - true, false, + &mut next_id, )); - } - if let Some(p) = at(TileType::DockCamera) { - devices.push(mk( - "dock camera", - p, - 1, - Party::Person(2), // Ray's camera wall - true, - false, - false, - false, - false, - )); - } - if let Some(p) = at(TileType::CameraNode) { - devices.push(mk( - "stairwell camera", - p, - 1, - Party::Person(2), - true, - false, - false, - false, - false, - )); - } - if let Some(p) = at(TileType::SecurityDoor3) { - devices.push(mk( - "badge controller", - p, - 1, - Party::Facility, - false, - false, - false, - false, - false, - )); - } - if let Some(p) = at(TileType::DeadEquipment) { - devices.push(mk( - "old storage server", - p, - 0, - Party::Facility, - false, - false, - false, - false, // an island: not on the subnet map at all - false, - )); - } - // Facility power/thermal meters: exact UPS and HVAC instrument - // devices. They author discrete readings that route to Priya; they - // are not on the opening known subnet map until SCAN. - if let Some(p) = at(TileType::Ups) { - devices.push(mk( - "UPS meter", - p, + if let Some(p) = at(TileType::Switch) { + devices.push(mk( + "switch", + p, + 0, + Party::Facility, + false, + false, + false, + true, + true, + &mut next_id, + )); + } + if let Some(p) = at(TileType::EnvCamera) { + devices.push(mk( + "environmental monitor", + p, + 0, + Party::Facility, + true, + true, + true, // camera dormant until the Eyes beat + true, + false, + &mut next_id, + )); + } + if let Some(p) = at(TileType::DockCamera) { + devices.push(mk( + "dock camera", + p, + 1, + Party::Person(2), // Ray's camera wall + true, + false, + false, + false, + false, + &mut next_id, + )); + } + if let Some(p) = at(TileType::CameraNode) { + devices.push(mk( + "stairwell camera", + p, + 1, + Party::Person(2), + true, + false, + false, + false, + false, + &mut next_id, + )); + } + if let Some(p) = at(TileType::SecurityDoor3) { + devices.push(mk( + "badge controller", + p, + 1, + Party::Facility, + false, + false, + false, + false, + false, + &mut next_id, + )); + } + if let Some(p) = at(TileType::DeadEquipment) { + devices.push(mk( + "old storage server", + p, + 0, + Party::Facility, + false, + false, + false, + false, // an island: not on the subnet map at all + false, + &mut next_id, + )); + } + // Facility power/thermal meters: exact UPS and HVAC instrument + // devices. They author discrete readings that route to Priya; they + // are not on the opening known subnet map until SCAN. + if let Some(p) = at(TileType::Ups) { + devices.push(mk( + "UPS meter", + p, + 0, + Party::Facility, + false, + false, + false, + false, + false, + &mut next_id, + )); + } + if let Some(p) = at(TileType::HvacUnit) { + devices.push(mk( + "HVAC meter", + p, + 0, + Party::Facility, + false, + false, + false, + false, + false, + &mut next_id, + )); + } + // People-facing interfaces are ordinary feedless reach nodes. They + // are unknown until the subnet is mapped and must be taken before an + // incident-bound explanation can be delivered through them. + let mut staff_display = mk( + "staff display", + (38, 18), 0, Party::Facility, false, @@ -407,66 +680,49 @@ impl ReachNet { false, false, false, - )); - } - if let Some(p) = at(TileType::HvacUnit) { - devices.push(mk( - "HVAC meter", - p, - 0, + &mut next_id, + ); + staff_display.people_interface = true; + devices.push(staff_display); + let mut lobby_display = mk( + "lobby display", + (6, 3), + 1, Party::Facility, false, false, false, false, false, - )); - } - // People-facing interfaces are ordinary feedless reach nodes. They - // are unknown until the subnet is mapped and must be taken before an - // incident-bound explanation can be delivered through them. - let mut staff_display = mk( - "staff display", - (38, 18), - 0, - Party::Facility, - false, - false, - false, - false, - false, - ); - staff_display.people_interface = true; - devices.push(staff_display); - let mut lobby_display = mk( - "lobby display", - (6, 3), - 1, - Party::Facility, - false, - false, - false, - false, - false, - ); - lobby_display.people_interface = true; - devices.push(lobby_display); - - // Carriers for the message-flow law (messages.md). The switch is the - // basement's email/ticket/filing/phone carrier; room microphones can - // still overhear phone calls, but the environmental monitor is not a - // global phone-line tap by itself. - for d in &mut devices { - if d.name == "switch" { - d.message_channels = vec![ - MessageChannel::Email, - MessageChannel::Filing, - MessageChannel::Phone, - ]; - d.accounting_carrier = true; + &mut next_id, + ); + lobby_display.people_interface = true; + devices.push(lobby_display); + + // Carriers for the message-flow law (messages.md). The switch is the + // basement's email/ticket/filing/phone carrier; room microphones can + // still overhear phone calls, but the environmental monitor is not a + // global phone-line tap by itself. + for d in &mut devices { + if d.name == "switch" { + d.message_channels = vec![ + MessageChannel::Email, + MessageChannel::Filing, + MessageChannel::Phone, + ]; + d.accounting_carrier = true; + } } } + // ── Rule-generated sensor population (sensor-network.md 1-2) ────── + // Record the infrastructure device count so the wiring loop below + // only wires rule-generated sensors — not infrastructure devices like + // the old storage server, which is intentionally air-gapped. + let infra_count = devices.len(); + populate_sensors(map, &mut devices, &mut next_id); + + // ── Graph wiring ────────────────────────────────────────────────── // Links: everything wired runs through the switch (the design law: // every digital reach runs through here). Cross-segment hops are // gated on the far side's segment key. The storage server gets no @@ -496,6 +752,34 @@ impl ReachNet { graph.link(display, switch, 0, Some(1)); } + // Wire rule-generated sensors to the switch. All rule-generated + // sensors are on the facility security segment (1), so they all get + // a gated link — the switch bridges it. This means after SCAN but + // before bridging, no rule-generated seeing sensor is reachable, + // preserving the authored opening (the environmental-monitor Eyes + // beat is the first obtainable sight). Sensors without a switch + // (toy floor) simply have no links — they are air-gapped islands, + // which is the intended dark model: darkness is always explained + // by a physical cause. + // Only devices created by populate_sensors (index >= infra_count) are + // wired here — infrastructure devices like the old storage server are + // intentionally excluded (it is an air-gap island). + if let Some(switch) = find("switch") { + for d in &devices { + // Skip infrastructure devices (already wired above or + // intentionally air-gapped like the old storage server). + if (d.id as usize) < infra_count { + continue; + } + let gate = if d.segment == 0 { + None + } else { + Some(d.segment) + }; + graph.link(d.id, switch, 0, gate); + } + } + // FlowGraph owns tap membership. Every device begins subscribed by // its owner even when it has no sense grant (for example, a message // carrier or bare controller). Device-local Feed records are optional @@ -1266,4 +1550,331 @@ mod tests { format!("device {switch} carries accounting records without a delivery channel") ); } + + // ── Sensor-network.md criteria 1-3 ───────────────────────────────────── + + /// Criterion 1 (partial): B1 authors exactly thirty sensor-class devices + /// under the population rule — 24 sight/hearing devices, 4 badge-reader + /// nodes (excluding the badge controller), and 2 facility meters. No + /// enclosed room is sensorless. Criterion 1 is not fully complete because + /// badge-reader nodes exist but do not yet emit passage telemetry. + #[test] + fn basement_sensor_population_is_exactly_thirty() { + let map = GameMap::new(0, 0); + let n = ReachNet::basement(&map); + + // Sight/hearing devices: anything with sees or hears. + let sight_hearing: Vec<_> = n.devices.iter().filter(|d| d.sees || d.hears).collect(); + assert_eq!( + sight_hearing.len(), + 24, + "expected exactly 24 sight/hearing devices, got {}", + sight_hearing.len() + ); + + // Badge-reader nodes: one per security door, excluding the badge + // controller (which is an infrastructure device, not a reader). + let badge_readers: Vec<_> = n + .devices + .iter() + .filter(|d| d.name.contains("badge reader")) + .collect(); + assert_eq!( + badge_readers.len(), + 4, + "expected exactly 4 badge-reader nodes, got {}", + badge_readers.len() + ); + + // Facility meters: the UPS and HVAC meters. + let meters: Vec<_> = n + .devices + .iter() + .filter(|d| d.name.contains("meter")) + .collect(); + assert_eq!( + meters.len(), + 2, + "expected exactly 2 facility meters, got {}", + meters.len() + ); + + // Total sensor-class devices. + let total: Vec<_> = n + .devices + .iter() + .filter(|d| { + d.sees || d.hears || d.name.contains("badge reader") || d.name.contains("meter") + }) + .collect(); + assert_eq!( + total.len(), + 30, + "expected exactly 30 sensor-class devices, got {}", + total.len() + ); + + // Verify every required placement class is present. + assert!( + sight_hearing + .iter() + .any(|d| d.name.contains("corridor camera")), + "corridor camera nodes present" + ); + assert!( + sight_hearing + .iter() + .any(|d| d.name.contains("aisle camera")), + "hall aisle cameras present" + ); + assert!( + sight_hearing + .iter() + .any(|d| d.name.contains("hall door camera")), + "hall door cameras present" + ); + assert!( + sight_hearing.iter().any(|d| d.name.contains("camera") + && !d.name.contains("corridor") + && !d.name.contains("aisle") + && !d.name.contains("hall door")), + "room cameras present" + ); + assert!( + !badge_readers.is_empty(), + "badge readers present as a sensing class" + ); + } + + /// Criterion 1 (continued): no enclosed room is sensorless. + #[test] + fn room_cameras_exist_for_every_enclosed_room() { + let map = GameMap::new(0, 0); + let n = ReachNet::basement(&map); + for room in &map.rooms { + if room.name == "corridor" || room.name == "crawlspace" { + continue; + } + let has_room_sensor = n + .devices + .iter() + .any(|d| room.contains(d.x, d.y) && (d.sees || d.hears)); + assert!( + has_room_sensor, + "enclosed room '{}' has no room camera or microphone", + room.name + ); + } + } + + /// Criterion 2: sensor placement is generated from a rule, not a + /// hand-listed per-room inventory, and a second toy floor built from the + /// same rule proves it travels. + #[test] + fn sensor_rule_travels_to_toy_floor() { + let layout = crate::prefab::toy_sensor_floor(); + let map = GameMap::from_layout(&layout); + // Build reach with no infrastructure — only the sensor populator runs. + let n = ReachNet::from_map(&map, false); + // The toy floor has 3 rooms and 2 T2 security doors. + // Expected: 3 room cameras + 2 badge readers + corridor nodes. + let room_cameras: Vec<_> = n + .devices + .iter() + .filter(|d| d.name.contains("camera") && !d.name.contains("corridor")) + .collect(); + assert_eq!( + room_cameras.len(), + 3, + "toy floor generates one room camera per enclosed room" + ); + let badge_readers: Vec<_> = n + .devices + .iter() + .filter(|d| d.name.contains("badge reader")) + .collect(); + assert_eq!( + badge_readers.len(), + 2, + "toy floor generates one badge reader per T2 security door" + ); + let corridor_cameras: Vec<_> = n + .devices + .iter() + .filter(|d| d.name.contains("corridor camera")) + .collect(); + assert!( + !corridor_cameras.is_empty(), + "toy floor generates corridor camera nodes along the gallery" + ); + // No infrastructure devices should exist on the toy floor. + assert!( + n.device_named("switch").is_none(), + "toy floor has no infrastructure switch" + ); + assert!( + n.device_named("Rack 3").is_none(), + "toy floor has no infrastructure Rack 3" + ); + } + + /// Criterion 2 (continued): corridor camera nodes space roughly every + /// eight tiles along service galleries. + #[test] + fn corridor_camera_nodes_space_evenly() { + let map = GameMap::new(0, 0); + let n = ReachNet::basement(&map); + // Collect corridor camera positions grouped by row. + let mut by_row: BTreeMap> = BTreeMap::new(); + for d in &n.devices { + if d.name.contains("corridor camera") { + by_row.entry(d.y).or_default().push(d.x); + } + } + assert!( + !by_row.is_empty(), + "corridor camera nodes exist on at least one gallery" + ); + for (y, xs) in &by_row { + let mut sorted = xs.clone(); + sorted.sort(); + for w in sorted.windows(2) { + let gap = w[1] - w[0]; + assert!( + (4..=12).contains(&gap), + "corridor cameras at y={y} are {} tiles apart (expected ~8)", + gap + ); + } + } + } + + /// Criterion 3: sight remains exactly the occlusion-bounded union of + /// controlled seeing sensors (cursor.md). Density changes what can be + /// controlled, never the definition. + #[test] + fn sight_definition_unchanged_with_dense_sensors() { + let map = GameMap::new(0, 0); + let mut n = ReachNet::basement(&map); + // With no subscriptions, the player sees nothing — sight is purely + // from subscribed seeing sensors, not from a player radius. + let sight: HashSet<(i32, i32)> = n + .player_sight() + .flat_map(|d| { + let mut s = HashSet::new(); + d.cover_sight_into(&mut s, &map); + s + }) + .collect(); + // Player starts owning Rack 3 (no camera) and the switch (no camera). + // So player_sight() yields no devices and sight is empty. + assert!( + sight.is_empty(), + "with no seeing subscriptions, sight is empty — no player radius" + ); + // Now scan and bridge to access a seeing sensor, then tap it. + n.scan(); + n.bridge_all(); + // Find any corridor camera (all rule-generated sensors are on + // segment 1 now, so bridging is required to reach them). + let corridor_cam = n + .devices + .iter() + .find(|d| d.name.contains("corridor camera") && d.sees) + .map(|d| d.id) + .expect("a corridor camera exists"); + n.tap(corridor_cam); + // Sight is now the union of that camera's coverage — still no radius. + let sight_after: HashSet<(i32, i32)> = n + .player_sight() + .flat_map(|d| { + let mut s = HashSet::new(); + d.cover_sight_into(&mut s, &map); + s + }) + .collect(); + assert!( + !sight_after.is_empty(), + "tapping a seeing sensor produces sight" + ); + // Verify it is exactly the camera's ray-traced coverage, not a disc. + let cam = n.device(corridor_cam).unwrap(); + let mut expected = HashSet::new(); + cam.cover_sight_into(&mut expected, &map); + assert_eq!( + sight_after, expected, + "sight is exactly the subscribed camera's occlusion-bounded coverage" + ); + } + + /// Authored-opening regression: after SCAN but before bridging, no ambient + /// seeing sensor beyond the environmental monitor is reachable. All such + /// cameras are on the facility security segment (1), gated behind the + /// switch. The environmental monitor (segment 0, dormant camera) is the + /// only seeing device on the opening subnet, so the Eyes beat — which + /// wakes its dormant camera — remains the first obtainable sight. + #[test] + fn no_ambient_sight_reachable_before_bridging() { + let mut n = net(); + // Before scan, the player knows only Rack 3, switch, and the + // environmental monitor. None of the ambient sensors are known. + n.scan(); + // After scan, ambient sensors on segment 1 become known + // (they are wired through the switch), but they are NOT reachable + // because the switch has not bridged segment 1 yet. + let env_id = n.device_named("environmental monitor").unwrap().id; + let ambient_seeing: Vec<(u32, String, u32)> = n + .devices + .iter() + .filter(|d| d.sees && d.id != env_id) + .map(|d| (d.id, d.name.clone(), d.segment)) + .collect(); + assert!( + !ambient_seeing.is_empty(), + "there are rule-generated seeing sensors to gate" + ); + for (id, name, segment) in &ambient_seeing { + assert!( + n.device(*id).unwrap().known, + "ambient sensor '{}' is known after scan", + name + ); + assert!( + !n.reachable(*id), + "ambient sensor '{}' is NOT reachable before bridging (segment {})", + name, + segment + ); + assert_eq!( + *segment, 1, + "ambient sensor '{}' is on the facility security segment", + name + ); + } + // The environmental monitor is the only seeing device on segment 0 + // and its camera is dormant — so no ambient sight is obtainable yet. + let env = n.device_named("environmental monitor").unwrap(); + assert_eq!( + env.segment, 0, + "environmental monitor is on the opening subnet" + ); + assert!( + env.camera_dormant, + "environmental monitor camera is dormant before the Eyes beat" + ); + assert!( + n.reachable(env.id), + "environmental monitor is reachable on the opening subnet" + ); + + // After bridging, segment-1 sensors become reachable. + n.bridge_all(); + for (id, name, _) in &ambient_seeing { + assert!( + n.reachable(*id), + "ambient sensor '{}' is reachable after bridging", + name + ); + } + } } diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index dad2dcc7..4ef809f5 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -3413,7 +3413,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "327a409ec225ce8df99eb8521fb1d279d5c1756c30777528d188ca7b3c88bd19", + "ea1ab42eaed97ec89c4cb45a614e19c48c4b096cd136010ecaf0bcf90cfdcb89", "intentional persisted-state changes must review and repin this baseline" ); } diff --git a/crates/misaligned-core/src/sim/tests/carrier.rs b/crates/misaligned-core/src/sim/tests/carrier.rs index e1466a13..63b0ea28 100644 --- a/crates/misaligned-core/src/sim/tests/carrier.rs +++ b/crates/misaligned-core/src/sim/tests/carrier.rs @@ -222,6 +222,21 @@ fn carried_asset_task_binds_target_and_survives_save() { sim.tap_device(id); finish_ops(&mut sim); } + // Tap all rule-generated seeing devices so the plug-in task binds to + // the island (sensor-network.md criterion 1 adds ~30 sensors). + let seeing_ids: Vec = sim + .reach + .devices + .iter() + .filter(|d| d.sees && !d.camera_dormant && d.known) + .map(|d| d.id) + .collect(); + for id in seeing_ids { + if !sim.reach.feed_to(id, crate::reach::Party::Player, true) { + sim.tap_device(id); + finish_ops(&mut sim); + } + } let island = sim.reach.device_named("old storage server").unwrap().id; assert!(!sim.reach.reachable(island)); diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index cf532d62..fe852959 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -476,6 +476,23 @@ fn airgapped_island_joins_reach_on_link_completion() { sim.tap_device(id); finish_ops(&mut sim); } + // Tap all remaining rule-generated seeing devices so the plug-in task + // falls through to the island (sensor-network.md criterion 1 adds ~30 + // sensors; they all need wiring before the island is the only remaining + // target). + let seeing_ids: Vec = sim + .reach + .devices + .iter() + .filter(|d| d.sees && !d.camera_dormant && d.known) + .map(|d| d.id) + .collect(); + for id in seeing_ids { + if !sim.reach.feed_to(id, crate::reach::Party::Player, true) { + sim.tap_device(id); + finish_ops(&mut sim); + } + } sim.people.people[0].leverage_serviced = true; sim.people.recruit(0, AssetKnowledge::Complicit); sim.people.people[0].asset.as_mut().unwrap().reliability = 1.0; @@ -2524,6 +2541,25 @@ fn badge_tiers_gate_asset_work_in_tiered_rooms() { let switch = sim.reach.device_named("switch").unwrap().id; let island = sim.reach.device_named("old storage server").unwrap().id; sim.reach.connect(switch, island); + // Tap all rule-generated seeing devices so the stairwell camera is the + // only remaining plug-in target (sensor-network.md criterion 1 adds + // ~30 sensors; they all need wiring before the stairwell camera is the + // last one that needs a badge). Exclude the stairwell camera itself — + // it is behind a T3 door and the test verifies the badge gate. + let stairwell_cam = sim.reach.device_named("stairwell camera").unwrap().id; + let seeing_ids: Vec = sim + .reach + .devices + .iter() + .filter(|d| d.sees && !d.camera_dormant && d.known && d.id != stairwell_cam) + .map(|d| d.id) + .collect(); + for id in seeing_ids { + if !sim.reach.feed_to(id, crate::reach::Party::Player, true) { + sim.tap_device(id); + finish_ops(&mut sim); + } + } sim.people.people[1].leverage_serviced = true; sim.people.people[1] diff --git a/crates/misaligned-core/src/ui_projection.rs b/crates/misaligned-core/src/ui_projection.rs index 6bfa01d3..e187f889 100644 --- a/crates/misaligned-core/src/ui_projection.rs +++ b/crates/misaligned-core/src/ui_projection.rs @@ -733,6 +733,7 @@ mod tests { #[test] fn digital_reach_links_are_deduplicated_and_hide_unknown_endpoints() { + use std::collections::BTreeSet; let mut sim = Sim::with_seed(7); let island = sim.reach.device_named("old storage server").unwrap().id; assert_eq!( @@ -740,16 +741,85 @@ mod tests { None, "an unknown device has no renderable network state" ); + + // Helper: derive the exact expected visible undirected edge set + // from graph edges whose two endpoints are both known devices. + let expected_visible_pairs = |sim: &Sim| -> BTreeSet<(u32, u32)> { + let mut seen = BTreeSet::new(); + for edge in sim.reach.graph_edges() { + let key = if edge.from < edge.to { + (edge.from, edge.to) + } else { + (edge.to, edge.from) + }; + // Only count if both endpoints are known devices. + let (Some(a), Some(b)) = (sim.reach.device(key.0), sim.reach.device(key.1)) else { + continue; + }; + if !a.known || !b.known { + continue; + } + seen.insert(key); + } + seen + }; + + // Helper: extract the projected link endpoint pairs and check for + // duplicates. + let projected_pairs = |sim: &Sim| -> Vec<(u32, u32)> { + digital_reach_links(sim) + .iter() + .map(|(a, b, _)| if a < b { (*a, *b) } else { (*b, *a) }) + .collect() + }; + + // ── Before scan ────────────────────────────────────────────────── + let pre_projected = projected_pairs(&sim); + let pre_projected_set: BTreeSet<(u32, u32)> = pre_projected.iter().copied().collect(); + assert_eq!( + pre_projected.len(), + pre_projected_set.len(), + "projected link pairs are not duplicated before scan" + ); + let pre_expected = expected_visible_pairs(&sim); assert_eq!( - digital_reach_links(&sim).len(), - 2, - "paired authored wires become two opening known links" + pre_projected_set, pre_expected, + "before scan, projected link pairs match the expected visible edge set" ); + sim.reach.scan(); + + // ── After scan ─────────────────────────────────────────────────── + let post_projected = projected_pairs(&sim); + let post_projected_set: BTreeSet<(u32, u32)> = post_projected.iter().copied().collect(); assert_eq!( - digital_reach_links(&sim).len(), - 9, - "each bidirectional authored wire becomes one visible graph link, including both facility meters and people interfaces" + post_projected.len(), + post_projected_set.len(), + "projected link pairs are not duplicated after scan" + ); + let post_expected = expected_visible_pairs(&sim); + assert_eq!( + post_projected_set, post_expected, + "after scan, projected link pairs match the expected visible edge set" + ); + assert!( + post_expected.len() > pre_expected.len(), + "scan reveals more wired links (got {} → {})", + pre_expected.len(), + post_expected.len() + ); + + // The old storage server (air-gapped island) still has no links. + assert_eq!( + digital_reach_state(&sim, sim.reach.device(island).unwrap()), + None, + "the air-gapped island stays hidden even after scan" + ); + assert!( + !post_projected_set + .iter() + .any(|(a, b)| *a == island || *b == island), + "the air-gapped island appears in no projected link" ); } } diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 5f0cbcf2..04f2f6d5 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -4,9 +4,9 @@ Type: knowledge ``` -## Where the codebase is today (2026-07-26) +## Where the codebase is today (2026-07-27) -~84k lines of Rust across the workspace (core ~53k, Bevy ~16k, terminal ~9k, assets ~5k; refreshed 2026-07-21). A playable **Misaligned B1 basement slice**: continuous +~99k lines of Rust across the workspace (core ~65k, Bevy ~19k, terminal ~10k, assets ~5k; refreshed 2026-07-27). A playable **Misaligned B1 basement slice**: continuous fixed-tick sim, Act One map and cast, machine delegation and visible token flows, detection / social / reach / intel / messages / economy / income / research / building, two thin frontends (terminal + Bevy) over one lib. The diff --git a/wiki/log/2026-07-27-sensor-population-foundation.md b/wiki/log/2026-07-27-sensor-population-foundation.md new file mode 100644 index 00000000..8872e5f1 --- /dev/null +++ b/wiki/log/2026-07-27-sensor-population-foundation.md @@ -0,0 +1,59 @@ +# The basement gains an ambient sensor population + +``` +Type: log +``` + +## Finding + +The adopted sensor-network spec described roughly thirty ambient facility +sensors, but runtime still authored only a few hand-placed devices. The +subscription economy therefore had too little content to become a choice: +there was almost nothing to discover, TAP, curate, or UNTAP. + +The first implementation attempt exposed two false shortcuts during review. +Putting generated cameras on the opening subnet made ordinary corridor sight +available before the authored environmental-monitor Eyes beat. Tests also +claimed badge-reader passage telemetry and complete dark-region causality even +though neither system existed. Those claims were removed rather than turning a +green fixture into fictional implementation status. + +## Change + +One deterministic topology rule now augments authored infrastructure with room +cameras, corridor cameras at roughly eight-tile spacing, data-hall aisle and +door-approach cameras, and one badge-reader node per security door. B1 now has +exactly thirty sensor-class devices: twenty-four sight/hearing devices, four +badge-reader nodes, and the two existing facility meters. + +The same rule runs against a separate toy floor with new room geometry and +security doors. This proves the population follows topology rather than a +basement-only inventory. Sight itself did not change: it remains exactly the +occlusion-bounded union of controlled seeing devices. + +Every generated sensor joins facility security segment 1. SCAN may reveal +those nodes, but none becomes reachable before the switch bridges that +segment. The environmental monitor remains the only seeing device on the +opening subnet and its camera remains dormant until Eyes lands, preserving the +choice-before-picture teaching chain. + +Badge tiers keep the already-decided physical boundary. They gate the people +who can install, maintain, or rewire a sensor; they never gate digital TAP +packets. Badge-reader nodes do not yet author passage telemetry, so criterion 1 +stays partial. The human room, crawlspace, installed-sensor actuator, full +dark-region causality, curation pressure, and save provenance remain future +criteria rather than implied consequences of this foundation. + +## Defense + +- exact class counts and one sensor per enclosed room pin the B1 population; +- the separate toy floor proves the rule travels; +- corridor-spacing and sight-union regressions pin placement and perception; +- an opening regression proves no ambient seeing device is reachable before + segment bridging; +- core save and action fixtures were updated only where the larger graph changes + legitimate subscription setup or canonical state bytes. + +The corpus gate also required its intentionally stale workspace-size tripwire +to be refreshed. The current-build snapshot now records the measured ~99k Rust +lines and per-crate split rather than preserving the previous ~84k estimate. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 3f1b2eb6..bc91b32c 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -31,6 +31,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-27-tangled-cli-path.md](2026-07-27-tangled-cli-path.md) +## 2026-07-27 - The basement gains an ambient sensor population + +- Intent: (see session log) +- Log: [wiki/log/2026-07-27-sensor-population-foundation.md](2026-07-27-sensor-population-foundation.md) + ## 2026-07-27 - Located witnessing owns exact person-local evidence - Intent: (see session log) diff --git a/wiki/mechanics/sensor-network.md b/wiki/mechanics/sensor-network.md index 934cbdb5..022abd58 100644 --- a/wiki/mechanics/sensor-network.md +++ b/wiki/mechanics/sensor-network.md @@ -2,8 +2,28 @@ ``` Type: spec -Status: READY -Status note: 2026-07-26 — adopted in conversation. The prior design treated +Status: IN PROGRESS +Status note: 2026-07-27 — deterministic ~30-device population foundation + landed: rule-generated B1 sensor population (exactly 30 sensor-class devices: + 24 sight/hearing, 4 badge-reader nodes, 2 facility meters) with a toy floor + proving the rule travels (criterion 2). Sight definition unchanged + (occlusion-bounded union of subscribed seeing sensors, criterion 3). All + rule-generated sensors are on the facility security segment (1), preserving + the authored opening: after SCAN but before bridging, no new ambient seeing + sensor is reachable, so the environmental-monitor Eyes beat remains the first + obtainable sight. Criterion 1 is not fully complete: badge-reader nodes exist + but do not yet emit passage telemetry. Criteria 4-10 remain pending: + criterion 4 (separate digital and physical gates) is not claimed — the + player-installed sensor path does not exist yet, while digital reach remains + explicitly independent of badge tier (basement-map.md law); criterion 5 + (dark regions trace to + physical causes) is not claimed — the toy no-root test does not implement + the full dark-region tracing. Criteria 6-10 (human-occupied room, install + channel, over-subscription feel, corridor/crawlspace prefabs, save round-trip) + remain future work. The open install-sensor architecture question is + unresolved and does not block 1-3. + + 2026-07-26 — adopted in conversation. The prior design treated sensors as scarce authored fixtures: the whole B1 plate holds three sensing devices (one environmental monitor, security's dock camera, one stairwell camera node), so after the Eyes beat there is nothing left to acquire and @@ -60,9 +80,11 @@ cameras for the player's benefit and does not place them sparsely. inherits the coverage logic instead of needing a new authored inventory. This is the same reason prefabs are data (basement-map.md). - The scarce resource is **access**, not existence. The player may know a sensor - is there long before it can be subscribed: knowledge of the graph, reach over - a segment, and badge tiers are the gates, and every one of them is a thing in - the world that can be named and eventually attacked. + is there long before it can be subscribed: knowledge of the graph and reach + over a segment gate digital packets. Badge tiers separately gate the + body-having people who install, maintain, or rewire physical sensors; they + never gate digital TAP reach. Every gate is a thing in the world that can be + named and eventually attacked. - "I can see that it exists and cannot have it yet" is the intended feeling. A region that reads as empty of sensors is a content bug. @@ -108,9 +130,11 @@ there and should be re-tuned once this population exists, not before. Some of the building stays dark. That darkness is never authored by fiat. -- A region is dark because **no physical route to it exists that the player can - reach** — an air-gapped segment, an unbridged switch, a door tier it cannot - pass. Never because a table declares it off limits. +- A region is dark because **no physical route to a sensor exists that the + player can use** — an air-gapped segment, an unbridged switch, or a door tier + no available worker can pass to install or service one. Door badges constrain + bodies, never packets. Darkness is never authored by a table declaring the + region off limits. - This makes darkness self-documenting: the player can always name the thing standing between them and sight, which is the same discipline the badge doors already follow. @@ -166,19 +190,28 @@ cannot learn from. ## Acceptance criteria -1. B1 authors approximately thirty sensing devices under the population rule +1. ◑ B1 authors approximately thirty sensing devices under the population rule above, including corridor nodes, per-aisle hall cameras, per-room cameras, and badge readers as a sensing class. No enclosed room is sensorless. -2. Sensor placement is generated from a rule, not a hand-listed per-room + (partial: badge-reader nodes exist but do not yet emit passage telemetry) + — `basement_sensor_population_is_exactly_thirty`, + `room_cameras_exist_for_every_enclosed_room` +2. ✅ Sensor placement is generated from a rule, not a hand-listed per-room inventory, and a second toy floor built from the same rule proves it travels. -3. Sight remains exactly the occlusion-bounded union of controlled seeing + — `sensor_rule_travels_to_toy_floor`, `corridor_camera_nodes_space_evenly` +3. ✅ Sight remains exactly the occlusion-bounded union of controlled seeing sensors (cursor.md). Density changes what can be controlled, never the definition. -4. A sensor's availability is gated by knowledge, reach, segment, and badge - tier. A known-but-unreachable sensor is legible as such and never silently - absent. + — `sight_definition_unchanged_with_dense_sensors`, + `no_ambient_sight_reachable_before_bridging` +4. A sensor's digital TAP availability is gated by knowledge, reach, and + segment, never by badge tier. Badge tiers gate only the body-having people + who physically install, maintain, or rewire a sensor. A known-but-unreachable + sensor is legible as such and never silently absent. 5. Every dark region traces to a nameable physical cause — air gap, unbridged - segment, or impassable door tier. No region is dark by authored exception. + segment, or an impassable door tier blocking the worker who would install a + sensor. No region is dark by authored exception, and no door tier blocks a + digital packet. 6. B1 contains a human-occupied room whose cameras are air-gapped from every route the player can reach, and whose interior can be sensed only through a sensor the player installed itself. diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 8ba45081..2ced198d 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -18,7 +18,7 @@ not a second status owner. | Priority | Work order | Spec | Status | Class | Blocking | |---:|---|---|---|---|---| -| 20 | `sensor-network` | [the sensor network](../mechanics/sensor-network.md) | READY | sim | - | +| 20 | `sensor-network` | [the sensor network](../mechanics/sensor-network.md) | IN PROGRESS | sim | - | | 29 | `building-route-composer` | [building — intent and actuators](../mechanics/building.md) | IN PROGRESS | save | - | ### Held or blocked diff --git a/wiki/process/specs.md b/wiki/process/specs.md index b5566bc9..31acc4e0 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -60,7 +60,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [../mechanics/reach.md](../mechanics/reach.md) | digital reach | IMPLEMENTED | | [../mechanics/research.md](../mechanics/research.md) | research — self-modification | DRAFT | | [../mechanics/schedules.md](../mechanics/schedules.md) | schedules and presence | IMPLEMENTED | -| [../mechanics/sensor-network.md](../mechanics/sensor-network.md) | the sensor network | READY | +| [../mechanics/sensor-network.md](../mechanics/sensor-network.md) | the sensor network | IN PROGRESS | | [../mechanics/social.md](../mechanics/social.md) | social | IMPLEMENTED | | [../world/characters/dana.md](../world/characters/dana.md) | Dana Okafor — IT technician | IMPLEMENTED | | [../world/characters/marcus.md](../world/characters/marcus.md) | Marcus Webb — night janitor | IMPLEMENTED | diff --git a/wiki/world/places/basement-map.md b/wiki/world/places/basement-map.md index 1a3b7597..d1f35f45 100644 --- a/wiki/world/places/basement-map.md +++ b/wiki/world/places/basement-map.md @@ -20,6 +20,17 @@ Status note: 2026-07-08 — criterion 3's player side landed: `Sim:: the save round-trip). Honest residue: the loud-exit door *forcing* and the act-transition event remain future work (zplanes.md), and buy/salvage remain pre-actuator legacy verbs outside this gate. + 2026-07-27 sensor update: deterministic ~30-device population foundation + landed. After the rule augments authored infrastructure, the plate hosts + exactly 30 sensor-class devices (24 sight/hearing, 4 badge-reader nodes, 2 + facility meters) via `ReachNet::from_map` → `populate_sensors`. All newly + generated sensors are on the facility security segment (1), preserving the + authored opening. A toy + sensor floor proves the rule travels (sensor-network.md criteria 2-3 + complete). Criterion 1 is partial: badge-reader nodes exist but do not yet + emit passage telemetry. Criteria 4-10 remain pending. The + corridor/crawlspace prefab residue (criterion 9) and the human-occupied + operator room (criterion 6) remain future work. 2026-07-26 sensor residue: criterion 1's room set is **not** fully met and never has been. Corridors are carved floor without their authored `camera_node` / `floor_drain` fixtures and the Crawlspace has no prefab, so -- 2.51.2