diff --git a/crates/misaligned-core/src/prefab.rs b/crates/misaligned-core/src/prefab.rs index e80856c2..a2b50bd9 100644 --- a/crates/misaligned-core/src/prefab.rs +++ b/crates/misaligned-core/src/prefab.rs @@ -506,7 +506,7 @@ const TOY_SENSOR_ROOM_A: Prefab = Prefab { #[cfg(test)] const TOY_SENSOR_ROOM_B: Prefab = Prefab { name: "toy_lab", - rows: &["#######", "#.....#", "#.....#", "#.....#", "##2####"], + rows: &["#######", "#.....#", "#..T..#", "#.....#", "##2####"], }; #[cfg(test)] diff --git a/crates/misaligned-core/src/reach.rs b/crates/misaligned-core/src/reach.rs index c5d8ceff..c0e11a5a 100644 --- a/crates/misaligned-core/src/reach.rs +++ b/crates/misaligned-core/src/reach.rs @@ -279,8 +279,13 @@ fn populate_sensors(map: &GameMap, devices: &mut Vec, next_id: &mut u32) if has_camera { continue; } - // Place the room camera at the room center. - let (cx, cy) = room.center(); + // Prefer the room center, but fixtures and other devices are allowed to + // occupy it. Keep the generated camera physical by selecting the + // nearest free ordinary floor tile instead of stacking it on whatever + // happens to be at the geometric center. + let Some((cx, cy)) = room_camera_position(map, room, devices) else { + continue; + }; let name = format!("{} camera", room.name); devices.push(make_sensor_device( *next_id, &name, cx, cy, true, // sees @@ -372,6 +377,19 @@ fn populate_sensors(map: &GameMap, devices: &mut Vec, next_id: &mut u32) } } +fn room_camera_position( + map: &GameMap, + room: &crate::prefab::Room, + devices: &[Device], +) -> Option<(i32, i32)> { + let center = room.center(); + (room.y..room.y + room.h) + .flat_map(|y| (room.x..room.x + room.w).map(move |x| (x, y))) + .filter(|&(x, y)| map.get_tile(x, y) == TileType::Floor) + .filter(|&(x, y)| !devices.iter().any(|device| (device.x, device.y) == (x, y))) + .min_by_key(|&(x, y)| ((x - center.0).abs() + (y - center.1).abs(), y, x)) +} + fn hall_door_camera_position( map: &GameMap, hall: &crate::prefab::Room, @@ -1570,11 +1588,11 @@ mod tests { // ── Sensor-network.md criteria 1-3 ───────────────────────────────────── - /// Criterion 1 (partial): B1 authors exactly thirty sensor-class devices + /// Criterion 1: 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. + /// enclosed room is sensorless; the passage-custody regression in + /// `sim::tests::sensor_network` pins the badge readers' live records. #[test] fn basement_sensor_population_is_exactly_thirty() { let map = GameMap::new(0, 0); @@ -1740,6 +1758,31 @@ mod tests { 3, "toy floor generates one room camera per enclosed room" ); + let lab = map.room_named("toy_lab").unwrap(); + let lab_camera = n.device_named("toy_lab camera").unwrap(); + assert_eq!( + map.get_tile(lab.center().0, lab.center().1), + TileType::LabBench + ); + assert_eq!( + (lab_camera.x, lab_camera.y), + (lab.center().0, lab.center().1 - 1), + "a center fixture moves the generated camera to the nearest free floor" + ); + assert_eq!(map.get_tile(lab_camera.x, lab_camera.y), TileType::Floor); + let office = map.room_named("toy_office").unwrap(); + let occupied_center = make_sensor_device( + u32::MAX, + "authored center device", + office.center().0, + office.center().1, + false, + ); + assert_eq!( + room_camera_position(&map, office, &[occupied_center]), + Some((office.center().0, office.center().1 - 1)), + "an existing center device moves the camera by the same stable tie break" + ); let badge_readers: Vec<_> = n .devices .iter() diff --git a/wiki/log/2026-07-27-room-camera-floor-placement.md b/wiki/log/2026-07-27-room-camera-floor-placement.md new file mode 100644 index 00000000..aed691a9 --- /dev/null +++ b/wiki/log/2026-07-27-room-camera-floor-placement.md @@ -0,0 +1,33 @@ +# Room cameras stay on physical floor + +``` +Type: log +Date: 2026-07-27 +Subject: sensor-network criterion 2 placement repair +``` + +The topology-generated sensor population traveled to another floor, but its +room-camera rule still treated a room's geometric center as usable without +reading the map or existing devices. That happens to work for the current B1 +rooms. It was not a rule that safely traveled: a later prefab could put a bench, +machine, drain, or another device at its center and receive a camera body and +sight origin stacked invisibly into that object. + +Room camera placement now searches the room for ordinary `Floor`, rejects a +coordinate already occupied by a device, and chooses the candidate nearest the +geometric center. Distance, then y, then x provide a stable tie break. The toy +floor's lab now deliberately places a non-walkable bench at its center; the +existing traveling-rule regression requires that camera to move one tile inward +onto the nearest free floor. Current basement camera coordinates and the exact +thirty-device inventory remain unchanged. + +The adjacent criterion-1 test comment also stopped claiming that badge passage +telemetry was absent; the ordinary simulation regression already pins that live +custody. + +Defense: `sensor_rule_travels_to_toy_floor` makes a center fixture part of the +fresh-topology fixture and requires the generated `toy_lab camera` to occupy the +deterministically nearest ordinary Floor tile. The same test places an existing +device on the ordinary Floor center of a second room and requires the same +stable fallback. A future regression to blind `room.center()` placement +therefore fails on topology independent of the B1 basement. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 160b9049..f6d82187 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -56,6 +56,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-27-schedules-evidence-custody.md](2026-07-27-schedules-evidence-custody.md) +## 2026-07-27 - Room cameras stay on physical floor + +- Intent: (see session log) +- Log: [wiki/log/2026-07-27-room-camera-floor-placement.md](2026-07-27-room-camera-floor-placement.md) + ## 2026-07-27 - Room approaches meet their doors - Intent: (see session log) diff --git a/wiki/mechanics/sensor-network.md b/wiki/mechanics/sensor-network.md index c428237d..05d1cb38 100644 --- a/wiki/mechanics/sensor-network.md +++ b/wiki/mechanics/sensor-network.md @@ -8,7 +8,8 @@ Status note: 2026-07-27 — criterion 1 is complete. A live player-controlled entered/left Presence records at that reader; initial state, impossible routes, and starved taps author nothing. Post-landing review also keeps every hall-door camera's body and sight origin on the first inward walkable hall - tile; every seeing + tile; room cameras use the nearest free ordinary floor when a fixture or + device occupies the room center; every seeing device, including topology-generated devices backed by ordinary Floor tiles, now resolves one same-coordinate Camera body in REAL. The deterministic ~30-device population foundation @@ -108,7 +109,7 @@ The basement carries approximately **thirty** sensing devices, by this rule: |---|---|---| | Corridor nodes | one `camera_node` roughly every eight tiles along both service galleries | ~8 | | Hall aisle cameras | one per cold aisle, plus the first inward walkable hall tile at each door approach | ~5 | -| Room cameras | one per enclosed room: closet, electrical, HVAC, both storages, janitor, wet lab, dock, stairwell, elevator | ~10 | +| Room cameras | one per enclosed room, at its center when free or the nearest free ordinary floor otherwise: closet, electrical, HVAC, both storages, janitor, wet lab, dock, stairwell, elevator | ~10 | | Badge readers | one per security door; a reader **sees who passed** even where no camera looks | ~6 | | Facility meters | the existing UPS and HVAC meters (power and thermal senses) | 2 | @@ -237,6 +238,8 @@ from. `tapped_badge_reader_authors_exact_enter_exit_presence_at_secured_door` 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. + Room cameras occupy a free ordinary floor tile nearest the room center; a + center fixture cannot become an invisible camera body or sight origin. — `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 @@ -281,6 +284,12 @@ channel already exists. the ordinary SCAN, bridge, TAP, and standing-drain path, then proves exact reader/person/room/door custody in both directions, no event below the person's door tier, and no event while a retained subscription is starved. +`sensor_rule_travels_to_toy_floor` places an authored lab bench at the toy +room's geometric center and requires its generated camera to move to the +deterministically nearest free Floor tile, preventing future prefabs from +silently stacking a camera body and sight origin on a fixture. The same test +places an existing device on another room's ordinary Floor center and pins the +same stable fallback. ## Open questions diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index cc96931b..618c79fe 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -22,7 +22,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), |---|---|---|---| | `wiki/process/tick.md` + issue automation | 2026-07-27 | finding | the queued retired-client violation held across binding procedure, both skill mirrors, four active prompt templates, tick intake, project status, and doctor. `tools/tangled_issues.py` now keeps repository discovery and authenticated issue writes on canonical Go `tg`, reconstructs deterministic display numbers from public issue records, folds public label-op history, and provides fail-closed structured list/view/create/edit/comment/close/label operations. A same-day live harvest exposed one wrong record assumption: comments are cross-account `sh.tangled.feed.comment` records linked through `subject.uri`, not issue-local child records. The repaired helper follows that backlink into the author's PDS and returns the markup text; a fixture pins the exact shape, and live issue #15 now returns Cameron's choice `1` — [migration and repair log](../log/2026-07-27-tangled-issue-client-migration.md) | | `wiki/world/places/basement-map.md` room topology | 2026-07-27 | finding | the queued five non-hall corridor cuts remained, and an executable all-room perimeter audit exposed the same dead-door shape at the loading-dock entry plus fixed objects blocking the interior faces of the roll door, HVAC door, both storage doors, Janitor door, and stairwell. Every non-hall approach now reaches its authored door from outside the prefab, every other perimeter tile remains closed, doorway interiors are clear, and the roll, sealed, tier-2, and tier-3 boundaries retain their exact kinds — [room-approach log](../log/2026-07-27-room-approaches-meet-doors.md). Prior [west-approach](../log/2026-07-26-west-hall-approach.md) and [hall-density](../log/2026-07-26-foundation-hall-density.md) repairs stand. | -| `wiki/mechanics/sensor-network.md` sensor population / criterion 1 | 2026-07-27 | finding | the re-audit found the exact remaining criterion-1 boundary: topology already authored four badge readers, but no passage event existed. A live reader TAP now resolves each authored room transition through the security-aware floor route from the loading dock, using the person's access tier, and records exact entered/left Presence custody at every crossed secured reader. Initial state, an impossible tier/path, and a starved retained tap remain silent. Criterion 1 is complete — [implementation](../log/2026-07-27-badge-reader-passage-custody.md), [prior audit](../log/2026-07-27-sim-advance-phase-defense.md), [population foundation](../log/2026-07-27-sensor-population-foundation.md) | +| `wiki/mechanics/sensor-network.md` generated sensor population / criteria 1-5 | 2026-07-27 | finding | criterion-2's traveling rule still placed every generated room camera at the geometric center without checking the tile or another device, so a later prefab could silently put its physical body and sight origin inside a fixture. Room cameras now choose the nearest free ordinary Floor tile with deterministic ties; the toy lab carries a center bench and pins the fallback. The same audit corrected the stale criterion-1 test comment left after exact badge passage custody landed — [placement repair](../log/2026-07-27-room-camera-floor-placement.md), [passage implementation](../log/2026-07-27-badge-reader-passage-custody.md), [prior audit](../log/2026-07-27-sim-advance-phase-defense.md), [population foundation](../log/2026-07-27-sensor-population-foundation.md) | | `wiki/interface/keymap.md` + terminal/Bevy input routes | 2026-07-26 | finding | the canonical table assigned `A` to left movement and only `e` / Enter to the context menu, but terminal still opened and closed menus with its older `a` alias and lacked the specified Shift+direction semantic jump. Terminal now implements WASD parity, `a` means left, `e` / Enter alone open the menu, and both frontends consume one renderer-neutral nearest-earned-anchor query without changing selection or opening a menu. README, action-vocabulary, terminal, context-menu, and pinned terminal hints now teach the same boundary — [log](../log/2026-07-26-terminal-keymap-a-reconciliation.md) | | `wiki/interface/action-vocabulary.md` + `ActionKind` registry | 2026-07-26 | finding | the exhaustive runtime registry and shared person/ACTIVE projections implemented `ActionKind::PlotPolicy`, but the canonical inventory omitted that live direct control. PLOT POLICY now names exact authored-route authorization, its generic `actions person ` / `act` route, and its disable-without-cancelling-submitted-work boundary; old Review/OpenEgress command variants remain correctly internal compatibility shapes rather than authored vocabulary — [log](../log/2026-07-26-action-vocabulary-plot-policy.md) | | `wiki/world/places/zplanes.md` + plane-stack substrate/API | 2026-07-26 | finding | criteria 1-2 remain implemented and criteria 3-6 honestly deferred, but the ratified plane-agnostic contract still left an unused `World::active()` simulation accessor plus active-plane comments on the B1 compatibility map path. The accessor is removed, map reads now say plane 0, the stale criterion/sensing comments are corrected, and a source-shape regression rejects restoration of simulation-owned floor selection — [log](../log/2026-07-26-zplanes-plane-agnostic-api-audit.md) |