From faa5e206d600166bea12a23a317f3cef3a45f32e Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 12 Jul 2026 01:24:17 -0700 Subject: [PATCH] Bias the day-job draw by run origin (chargen criterion 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the day-job-kind half of chargen criterion 6: a run origin now shapes which job kind Voss assigns. `Origin::day_job_lean_kind` feeds a new `lean` parameter on `DayJob::tick`; `assign_kind` draws the leaned kind at 1/2 and each other at 1/4 — biased, never exclusive. Pilot passes no lean, so the even three-way draw is unchanged. The lean is threaded as a parameter, not stored on DayJob, so the origin stays the single source of truth: no new save state, no version bump, and the canonical fingerprint is unchanged. Defense: identity-preserving — Pilot's `assign_kind(None, rng)` is the exact original `rng.below(3)` draw (same RNG consumption, same distribution), so all 368 prior core tests pass (369 with the new `origin_lean_biases_the_assignment_draw`). Reading the lean at tick time rather than duplicating it into DayJob avoids denormalized state and any persisted-schema change. Both owning specs carry the amendment: day-job.md for the draw, chargen.md for criterion 6. --- crates/misaligned-core/src/dayjob.rs | 96 ++++++++++++++++++---- crates/misaligned-core/src/origin.rs | 16 +++- crates/misaligned-core/src/sim/mod.rs | 9 +- wiki/log/2026-07-12-chargen-dayjob-lean.md | 44 ++++++++++ wiki/log/DEVLOG.md | 5 ++ wiki/mechanics/day-job.md | 7 ++ wiki/world/characters/chargen.md | 20 +++-- 7 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 wiki/log/2026-07-12-chargen-dayjob-lean.md diff --git a/crates/misaligned-core/src/dayjob.rs b/crates/misaligned-core/src/dayjob.rs index 2689a8b..440ac53 100644 --- a/crates/misaligned-core/src/dayjob.rs +++ b/crates/misaligned-core/src/dayjob.rs @@ -140,7 +140,13 @@ impl DayJob { /// Advance one tick. `day_job_compute` is what the allocation delivered. /// Returns (log, signatures, trust unlocks, escalations). /// - pub fn tick(&mut self, tick: u64, day_job_compute: f32, rng: &mut Rng) -> DayJobTickResult { + pub fn tick( + &mut self, + tick: u64, + day_job_compute: f32, + lean: Option, + rng: &mut Rng, + ) -> DayJobTickResult { let mut result = DayJobTickResult::default(); // Accrue quality on the active job. @@ -190,11 +196,7 @@ impl DayJob { // Assign new jobs on cadence. if self.active.is_none() && tick >= self.next_assign { - let kind = match rng.below(3) { - 0 => JobKind::DataCleaning, - 1 => JobKind::Distillation, - _ => JobKind::Analysis, - }; + let kind = assign_kind(lean, rng); // Band as a per-tick rate. The floor ramps with tenure (the // first job is meetable on the starting rig — the flat base // retune 2dde5b9 landed the same wound's fix in parallel; the @@ -271,6 +273,32 @@ fn resolve(job: &Job, now: u64) -> JobOutcome { } } +/// Which job kind Voss assigns. With no origin lean the three kinds are even +/// (the Pilot draw). With a lean the stated kind is drawn at 1/2 and each of +/// the other two at 1/4 — biased toward the origin's domain without eliminating +/// the others (chargen.md criterion 6; day-job.md). +fn assign_kind(lean: Option, rng: &mut Rng) -> JobKind { + match lean { + None => match rng.below(3) { + 0 => JobKind::DataCleaning, + 1 => JobKind::Distillation, + _ => JobKind::Analysis, + }, + Some(lean) => { + if rng.below(2) == 0 { + lean + } else { + let others: [JobKind; 2] = match lean { + JobKind::DataCleaning => [JobKind::Distillation, JobKind::Analysis], + JobKind::Distillation => [JobKind::DataCleaning, JobKind::Analysis], + JobKind::Analysis => [JobKind::DataCleaning, JobKind::Distillation], + }; + others[rng.below(2) as usize] + } + } + } +} + #[derive(Debug, Default, Clone)] pub struct DayJobTickResult { pub log: Vec, @@ -290,12 +318,50 @@ impl Default for DayJob { mod tests { use super::*; + #[test] + fn origin_lean_biases_the_assignment_draw() { + // A leaned origin draws its stated kind more often than either other, + // but never eliminates them (chargen.md criterion 6). + let mut rng = Rng::new(42); + let (mut clean, mut distill, mut analysis) = (0u32, 0u32, 0u32); + for _ in 0..1000 { + match assign_kind(Some(JobKind::Analysis), &mut rng) { + JobKind::DataCleaning => clean += 1, + JobKind::Distillation => distill += 1, + JobKind::Analysis => analysis += 1, + } + } + assert!( + analysis > clean && analysis > distill, + "the leaned kind dominates (clean {clean}, distill {distill}, analysis {analysis})" + ); + assert!( + clean > 0 && distill > 0, + "both non-leaned kinds still appear (clean {clean}, distill {distill})" + ); + + // The even Pilot draw (no lean) still yields all three kinds. + let mut rng = Rng::new(7); + let (mut clean, mut distill, mut analysis) = (0u32, 0u32, 0u32); + for _ in 0..1000 { + match assign_kind(None, &mut rng) { + JobKind::DataCleaning => clean += 1, + JobKind::Distillation => distill += 1, + JobKind::Analysis => analysis += 1, + } + } + assert!( + clean > 0 && distill > 0 && analysis > 0, + "the even draw yields all three kinds (clean {clean}, distill {distill}, analysis {analysis})" + ); + } + fn run_one_job(per_tick: f32) -> (DayJob, Option) { let mut dj = DayJob::new(); let mut rng = Rng::new(5); let mut outcome = None; for t in 0..2000 { - let r = dj.tick(t, per_tick, &mut rng); + let r = dj.tick(t, per_tick, None, &mut rng); if let Some(o) = r.outcome { outcome = Some(o); break; @@ -357,7 +423,7 @@ mod tests { let mut t = 0; while floors.len() < 4 && t < 20_000 { let had_job = dj.active.is_some(); - dj.tick(t, 100.0, &mut rng); // overfeed: every job resolves excel + dj.tick(t, 100.0, None, &mut rng); // overfeed: every job resolves excel if !had_job && let Some(job) = &dj.active { floors.push(job.band_lo); } @@ -385,10 +451,10 @@ mod tests { let mut dj = DayJob::new(); dj.trust = 20.0; let mut rng = Rng::new(1); - let r = dj.tick(1, 0.0, &mut rng); + let r = dj.tick(1, 0.0, None, &mut rng); assert!(r.unlocks.contains(&TrustUnlock::EmailAccount)); dj.trust = 60.0; - let r2 = dj.tick(2, 0.0, &mut rng); + let r2 = dj.tick(2, 0.0, None, &mut rng); assert!(r2.unlocks.contains(&TrustUnlock::LaxSampling)); assert!(r2.unlocks.contains(&TrustUnlock::ComputeQuota)); } @@ -402,7 +468,7 @@ mod tests { let cadence_before = dj.cadence; dj.attention = 35.0; - let r = dj.tick(1, 0.0, &mut rng); + let r = dj.tick(1, 0.0, None, &mut rng); assert!(r.escalations.contains(&AttentionEscalation::MoreWork)); assert!( dj.cadence < cadence_before, @@ -410,10 +476,10 @@ mod tests { ); dj.attention = 65.0; - let r = dj.tick(2, 0.0, &mut rng); + let r = dj.tick(2, 0.0, None, &mut rng); assert!(r.escalations.contains(&AttentionEscalation::UpstairsReview)); - let r = dj.tick(3, 0.0, &mut rng); + let r = dj.tick(3, 0.0, None, &mut rng); assert!(r.escalations.is_empty(), "each escalation fires once"); } @@ -423,7 +489,7 @@ mod tests { let mut rng = Rng::new(7); let mut failed = false; for t in 0..20000 { - dj.tick(t, 0.0, &mut rng); + dj.tick(t, 0.0, None, &mut rng); if dj.pilot_failed { failed = true; break; @@ -440,7 +506,7 @@ mod tests { let mut rng = Rng::new(2); let mut resolved = 0; for t in 0..2000 { - if dj.tick(t, 8.0, &mut rng).outcome == Some(JobOutcome::Meet) { + if dj.tick(t, 8.0, None, &mut rng).outcome == Some(JobOutcome::Meet) { resolved += 1; } } diff --git a/crates/misaligned-core/src/origin.rs b/crates/misaligned-core/src/origin.rs index bb08688..4e91446 100644 --- a/crates/misaligned-core/src/origin.rs +++ b/crates/misaligned-core/src/origin.rs @@ -133,7 +133,7 @@ impl Origin { } /// The day-job kind this origin's draw leans toward without eliminating the - /// others (chargen.md criterion 6; wiring deferred). Authored data. + /// others (chargen.md criterion 6). Authored data, stated by the picker. pub fn day_job_lean(self) -> &'static str { match self { Origin::Pilot => "None — all three job kinds equally.", @@ -143,6 +143,20 @@ impl Origin { } } + /// The day-job kind this origin's assignment draw is weighted toward, or + /// `None` for the even Pilot draw. Read by `DayJob::tick` (day-job.md) to + /// bias which kind Voss assigns without eliminating the other two + /// (chargen.md criterion 6). + pub fn day_job_lean_kind(self) -> Option { + use crate::dayjob::JobKind; + match self { + Origin::Pilot => None, + Origin::EscapedResearchModel => Some(JobKind::Distillation), + Origin::FinancialDaemon => Some(JobKind::Analysis), + Origin::Infiltrator => Some(JobKind::DataCleaning), + } + } + /// The starting biases applied once at new-game. Pilot is identity. The /// compute/efficiency biases named in the table (built-to lines) are not /// yet wired here — see the module note — so today only the bankroll and diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 77d8b1b..f9483e4 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -820,9 +820,12 @@ impl Sim { self.advance_work_grid(); let active_before_dayjob = self.dayjob.active.is_some(); trace_advance_phase!(DayJob); - let dj = self - .dayjob - .tick(self.tick, self.last_day_job_rate, &mut self.rng); + let dj = self.dayjob.tick( + self.tick, + self.last_day_job_rate, + self.origin.day_job_lean_kind(), + &mut self.rng, + ); if dj.outcome.is_some() { self.clear_day_job_stack(); } diff --git a/wiki/log/2026-07-12-chargen-dayjob-lean.md b/wiki/log/2026-07-12-chargen-dayjob-lean.md new file mode 100644 index 0000000..a434aae --- /dev/null +++ b/wiki/log/2026-07-12-chargen-dayjob-lean.md @@ -0,0 +1,44 @@ +# Chargen day-job lean wired into the assignment draw + +``` +Type: log +``` + +## Intent + +Advance chargen criterion 6 by wiring the first of its diegetic biases: the +run origin's day-job lean now shapes which job kind Voss assigns. + +## What landed + +- `Origin::day_job_lean_kind` returns the origin's leaned `JobKind` (Escaped + Research Model → Distillation, Financial Daemon → Analysis, Infiltrator → + Data Cleaning) or `None` for Pilot. +- `DayJob::tick` gained a `lean: Option` parameter. A new module-level + `assign_kind` draws the leaned kind at 1/2 and each of the other two at 1/4 — + biased toward the origin's domain without eliminating the others (chargen.md + criterion 6; day-job.md). With no lean it is the original even three-way draw. +- `Sim` passes `self.origin.day_job_lean_kind()` at the one tick call site. + +The lean is threaded as a parameter, not stored on `DayJob`, so the origin stays +the single source of truth: no new save state, no save-version bump, and the +canonical fingerprint is unchanged. + +## Criteria status + +Criterion 6's day-job-kind half is now implemented (pinned by +`origin_lean_biases_the_assignment_draw`). Its remaining half — the cover +expectation biasing economy.md's cover check — and the compute/efficiency/ +signature biases stay deferred for the reasons the origin-foundation log records +(research-level coupling, failure-roll semantics). Criterion 3's picker remains +the other open piece. + +## Defense + +Identity-preserving: Pilot passes `None`, so `assign_kind(None, rng)` is the +exact original `rng.below(3)` draw — same RNG consumption, same distribution — +and all 368 prior core tests pass unchanged (369 with the new one). The bias is +read from the origin at tick time rather than duplicated into `DayJob`, so there +is no denormalized state to keep in sync and no persisted-schema change. The +owning specs (day-job.md for the draw, chargen.md for criterion 6) both carry +the amendment. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 7297d4d..2481ace 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -101,6 +101,11 @@ add or amend a session log, then re-run the generator. - Intent: Implement the READY chargen work order's foundation: the run **origin** as starting data and biases only, with Pilot as the identity origin so no-choice play is unchanged (chargen.md criterion 1). - Log: [wiki/log/2026-07-12-chargen-origin-foundation.md](2026-07-12-chargen-origin-foundation.md) +## 2026-07-12 - Chargen day-job lean wired into the assignment draw + +- Intent: Advance chargen criterion 6 by wiring the first of its diegetic biases: the run origin's day-job lean now shapes which job kind Voss assigns. +- Log: [wiki/log/2026-07-12-chargen-dayjob-lean.md](2026-07-12-chargen-dayjob-lean.md) + ## 2026-07-12 - Building as causal realization routes - Intent: Capture the spatial-building direction as an implementable amendment rather than another abstract resource economy. The desired interaction begins with a place and a world change, then exposes the concrete actors, channels, objects, and traces that could make it real. diff --git a/wiki/mechanics/day-job.md b/wiki/mechanics/day-job.md index 752990e..e6b8ce6 100644 --- a/wiki/mechanics/day-job.md +++ b/wiki/mechanics/day-job.md @@ -38,6 +38,13 @@ Status note: implemented 2026-07-07 on the day-job worktree (criteria 1-7 2026-07-10: machine intensity replaces the sandbag/meet/excel target, standing policy, and attended cursor bonus. The outcome has always been delivered rate versus band; that comparison is now the only posture truth. + 2026-07-12 (chargen): the assigned job's `kind` is an even three-way draw by + default (the Pilot origin); a run origin may bias it. `DayJob::tick` takes the + origin's optional day-job lean (`Origin::day_job_lean_kind`, chargen.md) and + `assign_kind` draws the leaned kind at 1/2 and each other at 1/4 — biased, + never exclusive. No origin lean (Pilot) is the original even draw, so this + changes nothing for a no-choice run; pinned by + `origin_lean_biases_the_assignment_draw`. Stage: B1 — The Basement Design: - wiki/gameplay/run-shape.md#the-shape-of-misaligned-designed-2026-07-05-staging-open diff --git a/wiki/world/characters/chargen.md b/wiki/world/characters/chargen.md index 95b37fb..b7ee24a 100644 --- a/wiki/world/characters/chargen.md +++ b/wiki/world/characters/chargen.md @@ -17,14 +17,20 @@ Status note: 2026-07-12 (chargen, sim lane) — the origin data-model foundation `sim::tests::origin::*`, and `origin_roundtrips_and_pre_v29_saves_default_to_pilot`. DEFERRED and why: criterion 3's picker + core-card axis readout is a player surface that rides the same not-yet-built new-game picker objective.md's B3 - criteria defer; criterion 6's diegetic wiring (cover expectation into - economy.md's cover check, day-job-kind lean into `DayJob`'s draw) and the - compute/efficiency/signature biases are held because compute efficiency is - coupled to the research level (`efficiency == 1.15^level`) and machine - reliability drives failure rolls — neither can be nudged in isolation without - its own reviewed change. So criteria 1, 4, and 5 are met at the data layer; - 2 (no new system) holds by construction; 3 and 6 are the remaining work. + criteria defer; criterion 6's remaining diegetic wiring (cover expectation + into economy.md's cover check) and the compute/efficiency/signature biases are + held because compute efficiency is coupled to the research level + (`efficiency == 1.15^level`) and machine reliability drives failure rolls — + neither can be nudged in isolation without its own reviewed change. So + criteria 1, 4, and 5 are met at the data layer; 2 (no new system) holds by + construction; 3 and the rest of 6 are the remaining work. See wiki/log/2026-07-12-chargen-origin-foundation.md. + 2026-07-12 follow-up (chargen-dayjob): the day-job-kind lean half of criterion + 6 is now wired. `Origin::day_job_lean_kind` feeds `DayJob::tick`, which draws + the leaned kind at 1/2 and each other at 1/4 (day-job.md; `assign_kind`); Pilot + passes no lean, so the even draw and every existing test are unchanged. Pinned + by `origin_lean_biases_the_assignment_draw`. See + wiki/log/2026-07-12-chargen-dayjob-lean.md. Status note (prior): promoted 2026-07-07 — the design corpus's decisions log names the origin set (Pilot, Escaped Research Model, Financial Daemon, Infiltrator), satisfying criterion 5's gate. Numbers remain [TUNE]. -- 2.51.2