diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 7a122f11..46a679ee 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -4872,6 +4872,9 @@ impl Sim { let defer_reason = (task == AssetTask::DeferMaintenance && !self.maintenance_deferral_would_bite()) .then(|| "no standing power or thermal load is on the books to defer".to_string()); + let alter_review_reason = (task == AssetTask::AlterReview) + .then(|| self.alter_review_unavailable_reason()) + .flatten(); let records_reason = (task == AssetTask::RetrieveRecords) .then(|| self.storage_b_records_unavailable_reason()) .flatten(); @@ -4886,6 +4889,7 @@ impl Sim { .or(route_reason) .or(pending_reason) .or(defer_reason) + .or(alter_review_reason) .or(records_reason) .or_else(|| { self.sink_action_blocked_reason(&SinkFireEffect::AssetTask { diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index 2b91c6d2..9e634b23 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -1564,6 +1564,12 @@ impl Sim { self.push_log("No standing power or thermal load is on the books to defer."); return; } + if task == AssetTask::AlterReview + && let Some(reason) = self.alter_review_unavailable_reason() + { + self.push_log(reason); + return; + } if task == AssetTask::RetrieveRecords && let Some(reason) = self.storage_b_records_unavailable_reason() { @@ -1602,6 +1608,24 @@ impl Sim { ); } + pub(crate) fn alter_review_unavailable_reason(&self) -> Option { + if self.dayjob.altered_review { + return Some("the next review is already set to land as nominal".into()); + } + self.thought_sinks + .open_sinks() + .any(|sink| { + matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::AlterReview, + .. + } + ) + }) + .then(|| "a request to alter the next review is already in progress".into()) + } + pub(crate) fn suppress_oldest_job_anomaly(&mut self, person_id: u8) -> Option { if self.people.get(person_id).is_none_or(|person| { person.role != PersonRole::HandlerSupervisor || person.asset.is_none() @@ -2082,6 +2106,12 @@ impl Sim { )); return true; } + if task == AssetTask::AlterReview && self.dayjob.altered_review { + self.push_log( + "The next review was already set to land as nominal; the duplicate request changed nothing.", + ); + return true; + } let actor_access = person.access; if let AssetTask::Eliminate(expected_target) = task { let target_valid = matches!( diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index 0af3cda8..b674a214 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -4,7 +4,7 @@ use crate::detection::{ EVIDENCE_CREDIBILITY_BASELINE, EVIDENCE_CREDIBILITY_COVERED, EVIDENCE_CREDIBILITY_HARDENED, EvidenceCoverOutcome, EvidenceFilingState, }; -use crate::person::{AssetTaskTarget, Knowledge}; +use crate::person::{AssetTaskTarget, Knowledge, PersonRole}; use crate::persona::PersonaIntegrity; #[test] @@ -2140,6 +2140,98 @@ fn voss_delays_the_audit_and_alters_one_review() { ); } +#[test] +fn alter_review_reserves_one_next_review_slot_across_handlers() { + let mut sim = Sim::new(); + recruit_reliable(&mut sim, 4); + sim.people.people[4].knowledge = Knowledge::Schedule; + sim.people.people[3].role = PersonRole::HandlerSupervisor; + recruit_reliable(&mut sim, 3); + sim.people.people[3].knowledge = Knowledge::Schedule; + + sim.asset_task(4, AssetTask::AlterReview); + let pending = sim + .person_actions(3) + .into_iter() + .find(|action| action.command == ActionCommand::AssetTask(3, AssetTask::AlterReview)) + .expect("a second handler keeps the exact task visible"); + assert_eq!( + pending.disabled_reason.as_deref(), + Some("a request to alter the next review is already in progress") + ); + sim.asset_task(3, AssetTask::AlterReview); + assert_eq!( + sim.thought_sinks + .open_sinks() + .filter(|sink| matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::AlterReview, + .. + } + )) + .count(), + 1, + "one global review slot cannot accept parallel handler requests" + ); + + finish_ops(&mut sim); + assert!(sim.dayjob.altered_review); + assert_eq!(tasks_done(&sim, 4), 1); + assert_eq!(tasks_done(&sim, 3), 0); + let armed = sim + .person_actions(3) + .into_iter() + .find(|action| action.command == ActionCommand::AssetTask(3, AssetTask::AlterReview)) + .expect("the armed task stays visible"); + assert_eq!( + armed.disabled_reason.as_deref(), + Some("the next review is already set to land as nominal") + ); + sim.asset_task(3, AssetTask::AlterReview); + assert!( + !sim.thought_sinks.open_sinks().any(|sink| matches!( + sink.effect, + SinkFireEffect::AssetTask { + task: AssetTask::AlterReview, + .. + } + )), + "an armed review does not accept another reservoir" + ); + assert!(sim.apply_asset_task_paid(3, AssetTask::AlterReview)); + assert!(sim.dayjob.altered_review); + assert_eq!( + tasks_done(&sim, 3), + 0, + "a stale paid request revalidates the shared slot before applying" + ); + + for _ in 0..1000 { + if sim.dayjob.active.is_some() { + break; + } + sim.advance(); + } + if let Some(job) = &mut sim.dayjob.active { + job.deadline = sim.tick + 1; + job.quality = 0.0; + } else { + panic!("Voss assigned a job within the cadence window"); + } + run(&mut sim, 3); + assert!( + !sim.dayjob.altered_review, + "one deadline evaluation consumes the slot" + ); + let consumed = sim + .person_actions(3) + .into_iter() + .find(|action| action.command == ActionCommand::AssetTask(3, AssetTask::AlterReview)) + .expect("the task remains available after consumption"); + assert!(consumed.disabled_reason.is_none()); +} + /// Criterion 8: a complicit asset can remove an earned human only through an /// exact shared-room carried task. Success preserves the target as history, /// stops their activity, and immediately begins containment. diff --git a/wiki/log/2026-07-29-alter-review-single-slot.md b/wiki/log/2026-07-29-alter-review-single-slot.md new file mode 100644 index 00000000..356e2cc1 --- /dev/null +++ b/wiki/log/2026-07-29-alter-review-single-slot.md @@ -0,0 +1,36 @@ +# AlterReview owns one next-review slot + +``` +Type: log +``` + +## Intent + +Make the existing AlterReview promise exact when more than one recruited +HandlerSupervisor can act: one nominal next review must not become one pending +effect per handler. + +## Finding + +The action and resolver both treated each `(person, AlterReview)` sink as an +independent target. Two handlers could therefore fund parallel requests before +either fired, and the already-armed `DayJob.altered_review` flag did not close +the action afterward. Both successful resolutions could count as completed +asset work even though the flag represented only one pending consequence. + +## Implementation + +One shared legality read now owns the next-review slot. An open AlterReview +reservoir reserves it across all handlers; the armed day-job flag keeps it +closed until the next deadline consumes it. Human action rows remain visible +with the exact reason, and direct dispatch enters the same guard. The resolver +also rechecks the armed flag so stale or malformed duplicate paid work changes +nothing and earns no second completion receipt. + +## Defense + +`alter_review_reserves_one_next_review_slot_across_handlers` recruits two +handler-shaped assets and pins the complete lifecycle: one pending reservoir, +cross-handler disabled reason, one completed receipt, armed-state reason, +fire-time stale duplicate rejection, deadline consumption, and availability +afterward. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 0528f705..104ba13a 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -76,6 +76,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-29-dana-schedule-record-custody.md](2026-07-29-dana-schedule-record-custody.md) +## 2026-07-29 - AlterReview owns one next-review slot + +- Intent: Make the existing AlterReview promise exact when more than one recruited HandlerSupervisor can act: one nominal next review must not become one pending effect per handler. +- Log: [wiki/log/2026-07-29-alter-review-single-slot.md](2026-07-29-alter-review-single-slot.md) + ## 2026-07-28 - Wire route selection: auto-routed candidates - Intent: (see session log) diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index 1bd09ecd..75f656d8 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -354,6 +354,11 @@ clause (see wiki/log/2026-07-05-demolition.md). `next_audit_tick + 2000` via a one-shot `audit_deferred_until` the firing rule honors; AlterReview arms `DayJob.altered_review`, filing one deadline evaluation as nominal (Meet) regardless of the delivered rate. + That consequence is one shared next-review slot, not one slot per handler: + an open AlterReview reservoir reserves it, the armed flag keeps it closed, + and the deadline evaluation consumes it before another request can begin. + Completion revalidates the armed flag so a duplicate stale request spends + its Thought but cannot apply or count the task twice. - `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 diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 391f9e16..b03af609 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -45,7 +45,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/world/characters/priya.md` + Act One detection inventory | 2026-07-29 | finding | runtime, status metadata, and the observer table all gave Priya the exact B1 Power/Thermal/Paper/Financial evidence set, but her narrative, acceptance criterion, and Act One's complete detection sentence still omitted Financial and called the inventory three channels. All mirrors now distinguish the three facilities inputs from the additional exact Financial evidence routed from the accounting-carrier switch, without reviving Financial as a fifth message channel — [audit log](../log/2026-07-29-priya-financial-channel-audit.md). The role-shaped facilities tasks and prior implementation remain pinned — [implementation log](../log/2026-07-18-priya-implemented.md). | | `wiki/world/characters/dana.md` | 2026-07-29 | finding | fresh audit found the character page claiming an 08:00 start while the binding schedules spec and `People::act_one` place Dana on site 09:00–17:00; it also retained pooled-signature/roll wording after one-shot Network evidence moved to exact routed custody. Both schedule mirrors now match runtime, the exact 08:00/09:00/13:00/17:00 boundary is pinned, and the page plus role-shaped PlugInDevice comments distinguish delivery from Dana's later irreversible cadence read — [log](../log/2026-07-29-dana-schedule-record-custody.md). Prior implementation stands — [2026-07-18](../log/2026-07-18-dana-implemented.md). | | `wiki/world/characters/ray.md` | 2026-07-21 | finding | Fire 131 closes the pre-Marcus Storage B route without making Marcus retrieve his own leverage: Ray's independently recruitable night patrol now reaches the exact records box from 23:00–00:00, and shared RetrieveRecords work carries one sealed file into the bounded information inbox. Exact target/save, action-surface, schedule arrival, duplicate rejection, and process-before-knowledge behavior are pinned — [log](../log/2026-07-21-storage-b-records.md). Prior observer/PatrolRedirect implementation stands — [prior log](../log/2026-07-18-ray-implemented.md) | -| `wiki/world/characters/voss.md` | 2026-07-18 | finding | criterion 5 (DelayAudit one-shot deferred audit boundary honored by the visible date and the firing rule) and the AlterReview nominal-filing row landed as handler-gated tasks with save round-trip pins; only criterion 8's blood branch keeps the order READY — [log](../log/2026-07-18-voss-handler-tasks.md). Prior 2026-07-17 audit: 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/voss.md` | 2026-07-29 | finding | AlterReview now reserves its one shared next-review slot across every HandlerSupervisor: an open request or armed nominal review disables all exact rows, fire revalidates stale duplicates, one deadline consumes the slot, and the action then reopens — [log](../log/2026-07-29-alter-review-single-slot.md). Prior 2026-07-18 audit: criterion 5 (DelayAudit one-shot deferred audit boundary honored by the visible date and the firing rule) and the AlterReview nominal-filing row landed as handler-gated tasks with save round-trip pins; only criterion 8's blood branch kept the order READY — [log](../log/2026-07-18-voss-handler-tasks.md). Prior 2026-07-17 audit: 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 remained the honest READY gap. That 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-22 | clean | re-audit after Storage B records, financial mail, and routed evidence: all five criteria still match production. Person/observer id 0, Silent filing, processed-debt gates, both payoff routes, the three baseline asset effects, tier-3 key, Knowing floor 30, and own-observer LookAway remain exact; 8 focused Marcus tests plus both Act One payoff arms passed — [log](../log/2026-07-22-marcus-implementation-re-audit.md). Prior graduation and added decay pin: [2026-07-15](../log/2026-07-15-marcus-graduation.md) | | `wiki/engineering/current-build.md` | 2026-07-27 | finding | all 18 present-tense system rows agree with current runtime and corpus status, including v56, exact routed evidence, cover custody/wear, standing plot policies, and the topology-generated B1 sensor population. The closing boundary did not: it said containment itself was absent with the deferred overt ladder, although audit threshold, explicit loud acts, and located human removal already enter persisted containment, convince every observer, and end the run. It now separates that live terminal entry from the deferred playable threat panel, sieges, hostile cuts, hunter machines, defense, and open re-hide outcome — [log](../log/2026-07-27-current-build-overt-boundary.md). The recurring Rust-count gate remains current — [prior log](../log/2026-07-14-current-build-count.md) | | `wiki/mechanics/plots.md` | 2026-07-26 | finding | issue #12's recommended per-authored-route scope is implemented: an eligible plot verb confirms one exact typed Thought/money/signature envelope, save v55 persists it, each policy consumes standing compute, and economy pulses can re-submit only an ordinary currently legal start action for that same plot id. Category/person-wide policy and direct leverage servicing remain absent; criterion 11 and the work order are complete — [log](../log/2026-07-26-standing-plot-policies.md) | diff --git a/wiki/world/characters/voss.md b/wiki/world/characters/voss.md index a1eb2529..9a4ca1d4 100644 --- a/wiki/world/characters/voss.md +++ b/wiki/world/characters/voss.md @@ -105,6 +105,14 @@ channel or a newer anomaly. Suspicion, filings, and any evidence Voss already read remain. If none remains after the ordinary reliability check, it records no completed task. +`AlterReview` reserves one shared **next review**, not one independent effect +per handler. While any handler's request is filling, every other exact +AlterReview row stays visible but says that the request is already in progress. +After it lands, the row says that the next review is already set to nominal. +The next deadline evaluation consumes that one armed slot and restores the +action. The execution boundary rechecks the flag: a stale duplicate request +cannot alter another deadline or earn a second completed-task receipt. + Defense: `sim::tests::social_plot::elimination_is_located_persistent_and_immediately_loud` pins the shared action, overlapping schedule, exact carried target, save/load, co-location fire, stopped dossier, and immediate Convinced/containment result. @@ -120,6 +128,9 @@ pins the exact reservoir-to-routed-custody effect; and the in-flight binding in the current save. Terminal's `narration_tests::suppress_logs_task_is_agent_selectable` pins the matching `task suppress` protocol shortcut. +`sim::tests::social_plot::alter_review_reserves_one_next_review_slot_across_handlers` +pins cross-handler reservation, armed-state presentation, fire-time duplicate +revalidation, single consumption, and reopening after the deadline evaluation. ## Schedule