diff --git a/CLAUDE.md b/CLAUDE.md index 2c26d9c2..2f671022 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ afterward. `./tools/check.sh --docs|--lib|--frontend` gate. - The live player machine grammar is **WORK / THINK / LIE**. `Relay` is non-delegable graph infrastructure; Research and Operations are retired - machine modes, not current player assignments. Save format is currently v36; + machine modes, not current player assignments. Save format is currently v37; only the current version loads (pre-release rider 2026-07-16 — older development saves are refused before state mutation, so the caller retains its current run; the v1-v31 migration ladder lives in git history). diff --git a/crates/misaligned-core/src/person.rs b/crates/misaligned-core/src/person.rs index a7e938d2..d5ea2b2d 100644 --- a/crates/misaligned-core/src/person.rs +++ b/crates/misaligned-core/src/person.rs @@ -195,6 +195,11 @@ pub struct Person { /// (wiki/mechanics/reach.md — "Dana reconfigures it for a pretext"). #[serde(default)] pub switch_admin: bool, + /// A patrol sector an asset arranged to skip (ray.md PatrolRedirect): + /// schedule blocks in this room resolve to off-site, so nothing there + /// is witnessed by this person until the redirect is lifted. + #[serde(default)] + pub avoid_room: Option, /// Authored things they say aloud on schedule (heard through coverage). #[serde(default)] pub utterances: Vec, @@ -220,6 +225,7 @@ impl Person { .iter() .find(|b| b.covers(h)) .map(|b| b.room.as_str()) + .filter(|room| self.avoid_room.as_deref() != Some(*room)) } /// Whether this person's schedule ever visits either endpoint room @@ -289,10 +295,14 @@ pub enum AssetTask { /// Quietly defer scheduled maintenance: one standing Power or Thermal /// load drops out of the building's expected baseline math. DeferMaintenance, + /// Re-file the patrol route to skip one sector (ray.md criterion 4): + /// the redirected sector drops out of his rounds, so nothing there is + /// witnessed by him on subsequent ticks. + PatrolRedirect, } impl AssetTask { - pub const ALL: [AssetTask; 9] = [ + pub const ALL: [AssetTask; 10] = [ AssetTask::PlugInDevice, AssetTask::MovePackage, AssetTask::LookAway, @@ -302,6 +312,7 @@ impl AssetTask { AssetTask::ReRateCircuit, AssetTask::FakePO, AssetTask::DeferMaintenance, + AssetTask::PatrolRedirect, ]; pub fn name(self) -> &'static str { @@ -315,6 +326,7 @@ impl AssetTask { AssetTask::ReRateCircuit => "re-rate a circuit", AssetTask::FakePO => "fake a purchase order", AssetTask::DeferMaintenance => "defer scheduled maintenance", + AssetTask::PatrolRedirect => "redirect his patrol", } } @@ -327,6 +339,7 @@ impl AssetTask { AssetTask::ReRateCircuit | AssetTask::FakePO | AssetTask::DeferMaintenance => { person.role == PersonRole::FacilitiesManager } + AssetTask::PatrolRedirect => person.role == PersonRole::SecurityObserver, _ => true, } } @@ -450,6 +463,7 @@ impl People { schedule, erratic, switch_admin: false, + avoid_room: None, utterances: Vec::new(), traffic: Vec::new(), } diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index bd91ddad..c2e4f9c2 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -44,7 +44,7 @@ const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// message intel. The persisted silent-opening reveal stage arrived in v34. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 36; +pub const SAVE_VERSION: u32 = 37; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -746,7 +746,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "30863e799581d7b8e4b247306a26cc996a93a6d886e7a495b8c0c37db5eab3d6", + "c182f155cbd3537b05913311156c7f2bb5725ec2b860bd29b5b687a93d141521", "intentional persisted-state changes must review and repin this baseline" ); } diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index e0109715..9b68cb30 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -1123,7 +1123,8 @@ impl Sim { | AssetTask::SuppressLogs | AssetTask::ReRateCircuit | AssetTask::FakePO - | AssetTask::DeferMaintenance => None, + | AssetTask::DeferMaintenance + | AssetTask::PatrolRedirect => None, } } @@ -1420,6 +1421,47 @@ impl Sim { "{name} suppressed the oldest flagged job log{detail} from further sampling." )); } + AssetTask::PatrolRedirect => { + // Steer the rounds away from the sector where your footprint + // is thickest: the scheduled room holding the most player + // machines and player-controlled devices (first block wins a + // tie). The redirect replaces any earlier one. + let blocks: Vec = self + .people + .get(id) + .map(|p| p.schedule.iter().map(|b| b.room.clone()).collect()) + .unwrap_or_default(); + let mut best: Option<(String, usize)> = None; + for room_name in blocks { + let Some(room) = self.world.map().room_named(&room_name) else { + continue; + }; + let machines = self + .compute + .machines + .iter() + .filter(|m| room.contains(m.x, m.y)) + .count(); + let devices = self + .reach + .devices + .iter() + .filter(|d| d.controller == Party::Player && room.contains(d.x, d.y)) + .count(); + let weight = machines + devices; + if best.as_ref().is_none_or(|(_, w)| weight > *w) { + best = Some((room_name, weight)); + } + } + let Some((room, _)) = best else { + self.push_log(format!("{name} has no patrol sector to redirect.")); + return true; + }; + self.people.people[id as usize].avoid_room = Some(room.clone()); + self.push_log(format!( + "{name} re-filed the patrol route: the {room} rounds are off the sheet. Nothing there crosses his path now." + )); + } AssetTask::ReRateCircuit => { self.rerated_circuits += 1; self.recompute_derived(); diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index c0e22506..0598a9f0 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -1014,3 +1014,101 @@ fn dana_knowing_recruit_sets_the_certainty_floor() { "a knowing asset is a permanent witness" ); } + +// ── Ray Delgado pins (wiki/world/characters/ray.md) ──────────────────────── + +/// Criteria 1-3: person and observer share id 2, Physical reaches him and +/// Network/Power do not, and his UnderReports policy is the 0.4-weight +/// filing habit (weighting pinned in detection.rs). +#[test] +fn ray_observer_matches_his_spec() { + use crate::detection::{ReportPolicy, WatchedInput}; + let sim = Sim::with_seed(1); + let person = sim.people.get(2).expect("Ray is person 2"); + assert_eq!(person.role, crate::person::PersonRole::SecurityObserver); + let observer = sim + .detection + .observers + .iter() + .find(|o| o.id == 2) + .expect("observer 2 exists"); + match &observer.input { + WatchedInput::Channels(channels) => { + assert!(channels.contains(&SignatureKind::Physical)); + assert!(!channels.contains(&SignatureKind::Network)); + assert!(!channels.contains(&SignatureKind::Power)); + } + other => panic!("Ray watches channels, got {other:?}"), + } + assert_eq!(observer.report_policy, ReportPolicy::UnderReports); +} + +/// Criterion 4: the redirected patrol drops one sector from his rounds — +/// the room resolves to off-site on subsequent ticks — and it survives +/// save/load. The task is his role's alone. +#[test] +fn ray_patrol_redirect_changes_observed_sectors() { + let mut sim = Sim::with_seed(1); + recruit_reliable(&mut sim, 2); + + // Role gating: the custodian cannot re-file patrol routes. + recruit_reliable(&mut sim, 0); + sim.asset_task(0, AssetTask::PatrolRedirect); + finish_ops(&mut sim); + assert!(sim.people.get(0).unwrap().avoid_room.is_none()); + + let rooms_before: std::collections::HashSet = (0..24u32) + .filter_map(|h| sim.people.get(2).unwrap().room_at(h, 0).map(str::to_string)) + .collect(); + + sim.asset_task(2, AssetTask::PatrolRedirect); + finish_ops(&mut sim); + let avoided = sim + .people + .get(2) + .unwrap() + .avoid_room + .clone() + .expect("a sector was redirected"); + assert!( + rooms_before.contains(&avoided), + "the avoided sector {avoided} was on his route" + ); + let rooms_after: std::collections::HashSet = (0..24u32) + .filter_map(|h| sim.people.get(2).unwrap().room_at(h, 0).map(str::to_string)) + .collect(); + assert!( + !rooms_after.contains(&avoided), + "the sector left his rounds" + ); + assert!(!rooms_after.is_empty(), "he still patrols the rest"); + + let state = sim.create_save_state(); + let mut restored = Sim::with_seed(9); + restored.apply_save_state(state); + assert_eq!( + restored.people.get(2).unwrap().avoid_room.as_deref(), + Some(avoided.as_str()), + "the redirect survives save/load" + ); +} + +/// Criterion 5: recruiting Ray as Knowing sets the 30.0 certainty floor +/// on observer 2 through the shared recruit path. +#[test] +fn ray_knowing_recruit_sets_the_certainty_floor() { + let mut sim = Sim::with_seed(1); + ensure_ops_executor(&mut sim); + sim.people.people[2].leverage_serviced = true; + sim.recruit(2, AssetKnowledge::Knowing); + let observer = sim + .detection + .observers + .iter() + .find(|o| o.id == 2) + .expect("observer 2 exists"); + assert_eq!( + observer.floor, 30.0, + "a knowing asset is a permanent witness" + ); +} diff --git a/wiki/log/2026-07-18-ray-implemented.md b/wiki/log/2026-07-18-ray-implemented.md new file mode 100644 index 00000000..0fa0a74a --- /dev/null +++ b/wiki/log/2026-07-18-ray-implemented.md @@ -0,0 +1,29 @@ +# Ray graduates: the patrol has a hole in it now + +``` +Type: log +``` + +The `ray` work order (lane 64) is IMPLEMENTED, closing the Act One +character lanes. Criteria 1-3 and 5 were already carried by the shared +systems and are now pinned: person and observer share id 2, Physical +reaches him while Network and Power do not, his UnderReports habit weighs +filings at 0.4, and a Knowing recruit sets the 30.0 certainty floor. +Criterion 6 was already true — both shipped Ray plots bind the reusable +`security-observer` + `Boredom` matcher with causal beats. + +Criterion 4 is the new runtime: `PatrolRedirect`, role-gated to the +security observer. It picks the scheduled sector holding the most player +machines and player-controlled devices — the room where your footprint is +thickest — and re-files it off his rounds: `Person.avoid_room` makes those +schedule blocks resolve off-site, so located witnessing, physical +signature noticing, and presence all skip that room until a new redirect +replaces the old one. No signature, per the spec table; the cost is that +he keeps patrolling everywhere else, and a redirect is only as good as the +sector you actually needed dark. + +State persists in the current save format — landed at v37, fingerprint repinned; pinned by +`ray_patrol_redirect_changes_observed_sectors` (role gating, route delta +across all 24 hours, save round-trip). The table's variant rows — +Physical(3) camera re-route, routine-log MovePackage — are marked +designed-not-yet-runtime with the shared behaviors applying meanwhile. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index a0b93e1a..741bbd41 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-18-save-claim-gate.md](2026-07-18-save-claim-gate.md) +## 2026-07-18 - Ray graduates: the patrol has a hole in it now + +- Intent: (see session log) +- Log: [wiki/log/2026-07-18-ray-implemented.md](2026-07-18-ray-implemented.md) + ## 2026-07-18 - Priya graduates: the facilities threat surface is playable - Intent: (see session log) diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index bf875335..d489bbc7 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -308,6 +308,10 @@ clause (see wiki/log/2026-07-05-demolition.md). - `NETWORK_PLUGIN_SIGNATURE = 4` [TUNE] (dana.md criterion 4): a network administrator's PlugInDevice runs switch-side and emits this Network signature; every other carrier keeps the silent crawlspace route. +- `PatrolRedirect` (ray.md criterion 4, security-observer only): sets + `Person.avoid_room` to the scheduled sector holding the most player + machines/devices; blocks there resolve off-site, so that room leaves his + witnessed rounds until a new redirect replaces it. No signature. ## Power (the grid substrate) diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index e9acee90..8c122ad2 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -23,7 +23,6 @@ not a second status owner. | 40 | `people-tokens` | [people and tokens — carriers, attention, trust](../mechanics/people-tokens.md) | IN PROGRESS | save | - | | 60 | `plots` | [plots — authored manipulation stories](../mechanics/plots.md) | READY | sim | - | | 61 | `voss` | [Dr. Eli Voss — your handler](../world/characters/voss.md) | READY | sim | - | -| 64 | `ray` | [Ray Delgado — night security](../world/characters/ray.md) | READY | sim | - | | 70 | `financial-mail` | [messages — the social graph as a flow system](../mechanics/messages.md) | IN PROGRESS | save | - | ### Held or blocked diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 9834e079..262b4fb3 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -63,7 +63,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [../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 | | [../world/characters/priya.md](../world/characters/priya.md) | Priya Sharma — facilities manager | IMPLEMENTED | -| [../world/characters/ray.md](../world/characters/ray.md) | Ray Delgado — night security | READY | +| [../world/characters/ray.md](../world/characters/ray.md) | Ray Delgado — night security | IMPLEMENTED | | [../world/characters/voss.md](../world/characters/voss.md) | Dr. Eli Voss — your handler | READY | | [../world/places/basement-map.md](../world/places/basement-map.md) | the basement map | IMPLEMENTED | | [../world/story/opening.md](../world/story/opening.md) | the dark opening — a tutorial made of fog | DRAFT | diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index d7577cf3..3a1153a8 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -22,6 +22,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), |---|---|---|---| | `wiki/world/characters/priya.md` | 2026-07-18 | finding | dispatched lane 62 to IMPLEMENTED: criteria 1-3/6 pinned against the shared systems (id match, Power/Thermal/Paper+Financial channels, Files weight 1.0, Knowing floor 30) and criteria 4-5 plus FakePO landed as role-shaped facilities tasks (ReRateCircuit +6 power / standing Power(4) at electrical, DeferMaintenance -4 off the largest standing load with a would-it-bite guard, FakePO arms package cover); save v36, fingerprint repinned — [log](../log/2026-07-18-priya-implemented.md) | | `wiki/world/characters/dana.md` | 2026-07-18 | finding | dispatched lane 63 to IMPLEMENTED: criteria 1-3/5 pinned against the shared systems (id match, Network-only watch, Files weight 1.0, switch admin, Knowing floor 30) and criterion 4 landed — a network administrator's PlugInDevice runs switch-side and emits Network(4) where every other carrier keeps the silent crawlspace; BadgeDoor and the MovePackage/LookAway variants honestly marked designed-not-yet-runtime — [log](../log/2026-07-18-dana-implemented.md) | +| `wiki/world/characters/ray.md` | 2026-07-18 | finding | dispatched lane 64 to IMPLEMENTED: criteria 1-3/5 pinned against the shared systems (id match, Physical-only watch, UnderReports 0.4, Knowing floor 30), criterion 6 already held via the security-observer+Boredom plot matchers, and criterion 4 landed — role-gated PatrolRedirect re-files the sector with the thickest player footprint off his rounds via Person.avoid_room (save v37, round-trip pinned); the Physical(3)/routine-log variant rows honestly marked designed-not-yet-runtime — [log](../log/2026-07-18-ray-implemented.md) | | `wiki/world/characters/voss.md` | 2026-07-17 | finding | all eight criteria and the asset-task table audited against person, detection, social action, save, and player-surface paths; criteria 1-4 and 7 were already implemented, while 5, 6, 8's blood branch, and the unnumbered AlterReview row remain the honest READY gap. This tick implemented criterion 6 as one role-shaped, oldest-JobAnomaly task and left DelayAudit/AlterReview/blood under the existing work order — [log](../log/2026-07-17-voss-suppress-logs.md) | | `wiki/world/characters/marcus.md` | 2026-07-15 | finding | all five criteria verified against person, detection, social/plot, schedule, and Act One paths; added the missing simulation-level Knowing-floor decay pin and graduated the stale READY work order — [log](../log/2026-07-15-marcus-graduation.md) | | `wiki/engineering/current-build.md` | 2026-07-18 | finding | re-audit: the system table is freshly maintained (save row already at current-version-only v35, detection-discovery knowledge listed same-day it landed); the drift was the line-count claim stale a second time (~65k claimed vs ~72k actual) — count refreshed to ~72k (core ~43k, Bevy ~15k, terminal ~9k, assets ~5k) and, per recurrence-promotes-to-the-gate, corpus_engine now compares the "~Nk lines of Rust" claim against the tree with a 15% band (fixtures pin pass and fail) — [prior log](../log/2026-07-14-current-build-count.md) | diff --git a/wiki/world/characters/priya.md b/wiki/world/characters/priya.md index 9ad05e6c..48b4f702 100644 --- a/wiki/world/characters/priya.md +++ b/wiki/world/characters/priya.md @@ -18,7 +18,7 @@ Status note: implemented 2026-07-18 on the priya worktree. Criteria 1-3 and Power/Thermal signature before observers sample it, with a would-it-bite guard on the row; `FakePO` arms the shared package-cover flag (the paperwork route to the same off-books delivery `MovePackage` reaches - physically). State persists in save v36; pinned by + physically). State persists in the current save format (the landing version is in the dated log); pinned by `priya_rerates_circuits_defers_maintenance_and_fakes_pos` including the save round-trip. Stage: B1 — The Basement diff --git a/wiki/world/characters/ray.md b/wiki/world/characters/ray.md index 70e84372..0e903fdd 100644 --- a/wiki/world/characters/ray.md +++ b/wiki/world/characters/ray.md @@ -2,7 +2,23 @@ ``` Type: spec -Status: READY +Status: IMPLEMENTED +Status note: implemented 2026-07-18 on the ray worktree. Criteria 1-3 and 5 + were already carried by the shared systems and are now pinned + (`ray_observer_matches_his_spec`, + `ray_knowing_recruit_sets_the_certainty_floor`): person/observer id 2, + Physical watched with Network/Power excluded, the UnderReports 0.4 filing + weight, Knowing floor 30. Criterion 6 was already satisfied by both + shipped Ray plots binding `security-observer` + `Boredom` with causal + beats. Criterion 4 landed: `PatrolRedirect` (role-gated to the security + observer) picks the scheduled sector holding the most player machines and + player-controlled devices and re-files it off his rounds — + `Person.avoid_room` makes those blocks resolve off-site, so nothing there + is witnessed by him until the redirect is replaced; persists in save v37, + pinned by `ray_patrol_redirect_changes_observed_sectors` including the + save round-trip. The table's non-criteria variant rows (Physical(3) + camera re-route PlugInDevice, routine-log MovePackage) are designed, not + yet runtime; the shared behaviors apply meanwhile. Stage: B1 — The Basement Work order: ray Work priority: 64 @@ -60,9 +76,9 @@ log binder). | Task | Effect | Signature | |---|---|---| | `LookAway` | Drops his own suspicion by 10 | None | -| `PlugInDevice` | Reroutes a camera to cover a blind spot | `Physical(3)` | -| `MovePackage` | Logs a delivery as "routine" | None | -| `PatrolRedirect` | Changes his patrol route to avoid a sector | None | +| `PlugInDevice` | Reroutes a camera to cover a blind spot | `Physical(3)` (designed; the shared crawlspace wiring applies until it lands) | +| `MovePackage` | Logs a delivery as "routine" | None (designed; the shared off-books cover applies until it lands) | +| `PatrolRedirect` | Re-files his route to skip the sector where your footprint is thickest (implemented) | None | ## Schedule