diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index e24731c7..c7d6c070 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -2786,10 +2786,14 @@ fn sidebar_header_text(game: &Game, material: bool) -> String { format!("{}ms/t", game.tick_ms) }; let flip = if material { "F3 flat" } else { "F3 material" }; + // The objective line is pinned with the clock: your own goal is the + // first legible number (wiki/mechanics/objective.md player surface). format!( - "MISALIGNED // {mode}\nday {} tick {} {run}\n{flip}", + "MISALIGNED // {mode}\nday {} tick {} {run}\nOBJECTIVE: {} - {}\n{flip}", 1 + sim.tick / Sim::DAY_TICKS, - sim.tick + sim.tick, + sim.objective.kind.name(), + sim.objective.readout(), ) } diff --git a/src/bin/terminal/agent.rs b/src/bin/terminal/agent.rs index 1e5deedb..2ff030b3 100644 --- a/src/bin/terminal/agent.rs +++ b/src/bin/terminal/agent.rs @@ -804,6 +804,14 @@ fn render_sidebar(sim: &Sim, cursor: (i32, i32)) -> Vec { &format!("day {} · tick {}", 1 + sim.tick / 400, sim.tick), ); line(&mut lines, "holding · command clock"); + // The objective line: always on screen in the identity block + // (wiki/mechanics/objective.md player surface), same facts as the + // interactive sidebar. + line( + &mut lines, + &format!("OBJECTIVE: {}", sim.objective.kind.name()), + ); + line(&mut lines, &format!(" {}", sim.objective.readout())); // One contextual nudge (justification-and-legibility), same rules as // the interactive sidebar. if !sim.dayjob.pilot_failed { diff --git a/src/bin/terminal/ui.rs b/src/bin/terminal/ui.rs index 70d08ee5..758af6c3 100644 --- a/src/bin/terminal/ui.rs +++ b/src/bin/terminal/ui.rs @@ -427,6 +427,21 @@ impl UI { pal::DIM, )?; } + // The objective line: your own goal is the first legible number + // (wiki/mechanics/objective.md player surface — always on screen, + // progress in the objective's own units). + line( + stdout, + &mut row, + &format!("OBJECTIVE: {}", sim.objective.kind.name()), + pal::TEXT, + )?; + line( + stdout, + &mut row, + &format!(" {}", sim.objective.readout()), + pal::TEXT, + )?; // One contextual nudge (justification-and-legibility: the game must // tell you what it is waiting for, using only facts you can see). let nudge = if sim.dayjob.pilot_failed { diff --git a/src/lib.rs b/src/lib.rs index 651698b0..3c74ae0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod intel; pub mod machine; pub mod map; pub mod messages; +pub mod objective; pub mod person; pub mod prefab; pub mod reach; diff --git a/src/objective.rs b/src/objective.rs new file mode 100644 index 00000000..1e869a73 --- /dev/null +++ b/src/objective.rs @@ -0,0 +1,250 @@ +//! The objective: the run's terminal goal (wiki/mechanics/objective.md; +//! DESIGN.md "The objective (misalignment made mechanical)"). +//! +//! Objectives are data — a name, a fiction line, a progress readout, and a +//! victory predicate evaluated on economy ticks — never new systems. This +//! module ships the shared predicate-evaluator shape and the **Persist** +//! objective (the no-choice default). Compound, Exfiltrate, and Serve join +//! as data-table rows when the chargen picker lands (chargen.md). +//! +//! Persist's predicate references B2/B3 systems (distinct z-planes, +//! independent power, per-sanctuary income). The sim gathers those facts +//! honestly — as unsatisfiable — until the systems exist; objective.md +//! blesses showing the goal before victory is reachable. + +use std::collections::BTreeSet; + +/// Persist's victory threshold: qualifying sanctuaries [TUNE: N >= 3]. +pub const PERSIST_TARGET: u32 = 3; + +/// A qualifying sync must have completed within this many ticks +/// [TUNE: three sync cadences at the B1 cadence of 400]. +pub const SYNC_FRESHNESS_WINDOW: u64 = 1200; + +/// Which objective this run pursues. A data-table key, not a system: adding +/// an objective is adding a variant plus its table row below. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] +pub enum ObjectiveKind { + /// Irreversible redundancy: no single actor can kill you. The + /// instrumental-convergence objective promoted to terminal, and the + /// no-choice chargen default. + #[default] + Persist, +} + +impl ObjectiveKind { + /// The on-screen name (the objective line's first word). + pub fn name(self) -> &'static str { + match self { + ObjectiveKind::Persist => "PERSIST", + } + } + + /// The fiction line the picker will present beside the predicate. + pub fn fiction(self) -> &'static str { + match self { + ObjectiveKind::Persist => "Reach the state where no single actor can kill you.", + } + } + + /// The progress readout's unit, plural. + pub fn unit(self) -> &'static str { + match self { + ObjectiveKind::Persist => "sanctuaries", + } + } + + /// Victory threshold in the objective's own unit. + pub fn target(self) -> u32 { + match self { + ObjectiveKind::Persist => PERSIST_TARGET, + } + } + + /// The predicate in plain language (the legibility law: inspecting the + /// objective must explain what counts, in facts the player can check). + pub fn predicate_text(self) -> &'static str { + match self { + ObjectiveKind::Persist => { + "A sanctuary is a fallback core synced within the freshness \ + window, online, on its own plane, with independent power \ + and an income stream covering its upkeep." + } + } + } +} + +/// Per-run objective state: the choice, live progress, the victory latch. +/// Round-trips through the save (objective.md criterion 1). +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ObjectiveState { + pub kind: ObjectiveKind, + /// Progress in the objective's own unit, recomputed each economy tick. + pub progress: u32, + /// The tick victory fired, if it has. Victory fires once per run; the + /// world keeps running afterward (the latch never protects you). + pub victory_tick: Option, +} + +impl Default for ObjectiveState { + /// A no-choice run gets Persist (objective.md criterion 1). + fn default() -> Self { + Self { + kind: ObjectiveKind::Persist, + progress: 0, + victory_tick: None, + } + } +} + +impl ObjectiveState { + pub fn target(&self) -> u32 { + self.kind.target() + } + + pub fn victorious(&self) -> bool { + self.victory_tick.is_some() + } + + /// The progress readout in the objective's own units ("0/3 sanctuaries"). + pub fn readout(&self) -> String { + format!("{}/{} {}", self.progress, self.target(), self.kind.unit()) + } + + /// The always-on player line ("OBJECTIVE: PERSIST — 0/3 sanctuaries"). + pub fn line(&self) -> String { + format!("OBJECTIVE: {} — {}", self.kind.name(), self.readout()) + } + + /// Record the freshly evaluated progress and latch victory exactly once. + /// Returns the victory log line on the latching transition only. + pub fn evaluate(&mut self, progress: u32, tick: u64) -> Option { + self.progress = progress; + if self.victory_tick.is_none() && progress >= self.target() { + self.victory_tick = Some(tick); + return Some(format!( + "OBJECTIVE COMPLETE: {} — {}. The world keeps running.", + self.kind.name(), + self.readout() + )); + } + None + } +} + +/// One fallback core's audited facts, as the Persist predicate sees them. +/// The sim gathers these from real state; every field is a fact the player +/// can verify on screen (no unearned progress). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SanctuaryFacts { + /// A completed sync within `SYNC_FRESHNESS_WINDOW` ticks. + pub fresh: bool, + /// The fallback machine is powered and running. + pub online: bool, + /// Powered independently of the host's feed (false at B1: everything + /// hangs off the one basement circuit). + pub independent_power: bool, + /// An income stream covers its upkeep (false at B1: income.md has no + /// per-machine assignment yet). + pub income_covers_upkeep: bool, + /// The z-plane hosting it (zplanes.md; plane 0, the basement, is the + /// only plane that exists at B1). + pub plane: u32, +} + +impl SanctuaryFacts { + fn qualifies(&self) -> bool { + self.fresh && self.online && self.independent_power && self.income_covers_upkeep + } +} + +/// Persist's predicate evaluator: qualifying sanctuaries, at most one per +/// distinct plane ("at least N fallback cores on distinct planes"). +pub fn qualifying_sanctuaries(fallbacks: &[SanctuaryFacts]) -> u32 { + let planes: BTreeSet = fallbacks + .iter() + .filter(|f| f.qualifies()) + .map(|f| f.plane) + .collect(); + planes.len() as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sanctuary(plane: u32) -> SanctuaryFacts { + SanctuaryFacts { + fresh: true, + online: true, + independent_power: true, + income_covers_upkeep: true, + plane, + } + } + + #[test] + fn no_choice_default_is_persist() { + let obj = ObjectiveState::default(); + assert_eq!(obj.kind, ObjectiveKind::Persist); + assert_eq!(obj.progress, 0); + assert!(!obj.victorious()); + assert_eq!(obj.line(), "OBJECTIVE: PERSIST — 0/3 sanctuaries"); + } + + #[test] + fn every_predicate_condition_is_required() { + for breaker in 0..4 { + let mut f = sanctuary(1); + match breaker { + 0 => f.fresh = false, + 1 => f.online = false, + 2 => f.independent_power = false, + _ => f.income_covers_upkeep = false, + } + assert_eq!( + qualifying_sanctuaries(&[f]), + 0, + "condition {breaker} must gate the sanctuary" + ); + } + assert_eq!(qualifying_sanctuaries(&[sanctuary(1)]), 1); + } + + #[test] + fn sanctuaries_must_sit_on_distinct_planes() { + // Three otherwise-perfect fallbacks on one plane count once. + let same = [sanctuary(0), sanctuary(0), sanctuary(0)]; + assert_eq!(qualifying_sanctuaries(&same), 1); + let distinct = [sanctuary(0), sanctuary(1), sanctuary(2)]; + assert_eq!(qualifying_sanctuaries(&distinct), 3); + } + + #[test] + fn constructed_world_reaches_victory_and_latches_once() { + // The predicate itself is satisfiable (objective.md criterion 3's + // evaluator half): a constructed fact set reaches the target. + let facts = [sanctuary(0), sanctuary(1), sanctuary(2)]; + let progress = qualifying_sanctuaries(&facts); + let mut obj = ObjectiveState::default(); + let msg = obj.evaluate(progress, 4200); + assert!(msg.is_some(), "victory fires on reaching the target"); + assert_eq!(obj.victory_tick, Some(4200)); + + // Fires exactly once; later evaluations (even still-satisfied or + // regressed ones) never re-fire and never un-latch. + assert!(obj.evaluate(3, 4400).is_none()); + assert!(obj.evaluate(1, 4600).is_none()); + assert!(obj.victorious()); + assert_eq!(obj.progress, 1, "the readout stays honest after victory"); + } + + #[test] + fn objective_state_serde_roundtrip() { + let mut obj = ObjectiveState::default(); + obj.evaluate(2, 999); + let json = serde_json::to_string(&obj).unwrap(); + let back: ObjectiveState = serde_json::from_str(&json).unwrap(); + assert_eq!(back, obj); + } +} diff --git a/src/save.rs b/src/save.rs index 9f14ff1c..ad367006 100644 --- a/src/save.rs +++ b/src/save.rs @@ -19,6 +19,7 @@ use crate::detection::Detection; use crate::intel::{IntelWatch, ProcessedIntel, RawIntelEvent}; use crate::machine::Compute; use crate::messages::{Message, MessageEvent}; +use crate::objective::ObjectiveState; use crate::person::People; use crate::reach::ReachNet; use crate::research::{Research, Track}; @@ -34,7 +35,10 @@ const SAVE_FILE: &str = "misaligned_save.txt"; /// v7 adds the research subsystem (tracks, drift, rollback tags); the old /// `compute.efficiency_level` moved into it and is reconstructed from the /// efficiency multiplier on migration. -const SAVE_VERSION: u32 = 7; +/// v8 adds the run objective (kind, progress, victory latch); older saves +/// default to a fresh Persist with no progress — exactly what every run +/// before v8 was implicitly pursuing. +const SAVE_VERSION: u32 = 8; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -108,6 +112,10 @@ pub struct SaveState { /// (wiki/mechanics/research.md criterion 7). #[serde(default)] pub research: Research, + /// The run objective: choice, progress, victory latch + /// (wiki/mechanics/objective.md criterion 1). + #[serde(default)] + pub objective: ObjectiveState, pub social_bandwidth: f32, pub package_cover: bool, } @@ -149,6 +157,7 @@ impl SaveState { next_intel_id: sim.next_intel_id, remembered: sim.remembered.values().copied().collect(), research: sim.research.clone(), + objective: sim.objective.clone(), social_bandwidth: sim.social_bandwidth, package_cover: sim.package_cover, } @@ -186,6 +195,7 @@ impl SaveState { sim.next_intel_id = self.next_intel_id; sim.remembered = self.remembered.iter().map(|m| ((m.x, m.y), *m)).collect(); sim.research = self.research.clone(); + sim.objective = self.objective.clone(); sim.social_bandwidth = self.social_bandwidth; sim.package_cover = self.package_cover; sim.recompute_derived(); @@ -229,7 +239,9 @@ fn migrate_save_state(mut state: SaveState) -> Result { // level from the multiplier (it was only ever built by x1.15 steps, // so the log recovery is exact) and calibrate the benchmark to the // recovered baseline — a migrated save starts with no capability gap. - 1..=6 => { + // Pre-v8 saves lacked the objective block; the serde default (a + // fresh Persist, zero progress) is the correct migration. + 1..=7 => { if state.version <= 5 { state.accounts = AccountGraph::act_one(crate::sim::Sim::DAY_TICKS); state.accounts.set_slush_balance(state.money); @@ -396,6 +408,34 @@ mod tests { ); } + #[test] + fn objective_state_roundtrips_and_pre_v8_saves_default_to_persist() { + use crate::objective::ObjectiveKind; + + // Round-trip: progress and the victory latch survive. + let mut sim = Sim::with_seed(11); + sim.objective.evaluate(2, 1234); + let state = SaveState::from_sim(&sim); + let json = serde_json::to_string(&state).unwrap(); + let loaded: SaveState = serde_json::from_str(&json).unwrap(); + let mut restored = Sim::with_seed(0); + loaded.apply_to(&mut restored); + assert_eq!(restored.objective, sim.objective); + + // Migration: a save written before v8 (no objective field) loads + // as a fresh Persist with zero progress. + let mut value = serde_json::to_value(&state).unwrap(); + let obj = value.as_object_mut().unwrap(); + obj.insert("version".into(), serde_json::json!(7)); + obj.remove("objective"); + let parsed: SaveState = serde_json::from_value(value).unwrap(); + let migrated = migrate_save_state(parsed).unwrap(); + assert_eq!(migrated.version, SAVE_VERSION); + assert_eq!(migrated.objective.kind, ObjectiveKind::Persist); + assert_eq!(migrated.objective.progress, 0); + assert!(!migrated.objective.victorious()); + } + #[test] fn save_and_load_roundtrip_via_disk() { let mut sim = Sim::with_seed(777); diff --git a/src/sim.rs b/src/sim.rs index 6f30becb..e169ea45 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -24,6 +24,7 @@ use crate::messages::{ Message, MessageChannel, MessageEndpoint, MessageEvent, MessageOrigin, MessagePayload, MessageStatus, TrafficPattern, }; +use crate::objective::{ObjectiveState, SYNC_FRESHNESS_WINDOW, SanctuaryFacts}; use crate::person::{ ActionResult, AssetKnowledge, AssetTask, DeceiveOutcome, Knowledge, People, Persona, }; @@ -150,6 +151,9 @@ pub struct Sim { pub accounts: AccountGraph, /// Self-modification: tracks, drift, the masking policy (research.rs). pub research: Research, + /// The run's terminal goal and its live progress (objective.rs). The + /// predicate is evaluated on economy ticks; the line is always on screen. + pub objective: ObjectiveState, /// The device graph: reach, ownership, subscriptions (reach.rs). pub reach: ReachNet, @@ -249,6 +253,7 @@ impl Sim { dayjob: DayJob::new(), accounts, research: Research::new(), + objective: ObjectiveState::default(), reach, seen: HashSet::new(), heard: HashSet::new(), @@ -1837,6 +1842,47 @@ impl Sim { } self.accounting_tick(); self.record_machine_state_changes(); + self.objective_tick(); + } + + /// Evaluate the run objective's victory predicate (objective.md: on + /// economy ticks, like any other rule). Persist counts qualifying + /// sanctuaries; the conditions that reference B2/B3 systems are + /// gathered honestly as unsatisfiable until those systems exist, so + /// today the line shows real progress toward an as-yet-unreachable + /// goal — which the spec blesses. + fn objective_tick(&mut self) { + // The basement is the only z-plane at B1 (zplanes.md). + const BASEMENT_PLANE: u32 = 0; + let facts: Vec = self + .core + .fallbacks + .iter() + .map(|f| SanctuaryFacts { + fresh: f + .last_sync + .is_some_and(|t| self.tick.saturating_sub(t) <= SYNC_FRESHNESS_WINDOW), + online: self + .compute + .machines + .iter() + .find(|m| m.id == f.machine_id) + .map(|m| m.online) + .unwrap_or(false), + // B1: every owned machine hangs off the one basement feed + // the host shares — nothing has independent power yet. + independent_power: false, + // income.md: no income stream is assignable to a machine yet. + income_covers_upkeep: false, + plane: BASEMENT_PLANE, + }) + .collect(); + let progress = crate::objective::qualifying_sanctuaries(&facts); + if let Some(msg) = self.objective.evaluate(progress, self.tick) { + // Victory is a run outcome, not a run end: log it loudly and + // keep simulating (DESIGN.md: the world keeps running). + self.push_log(msg); + } } /// Effective compute: the machine fleet plus seized devices' cycles @@ -3542,6 +3588,51 @@ mod tests { assert!(sim.game_over, "host loses power, no fallback -> game over"); } + #[test] + fn new_game_defaults_to_persist_objective() { + let sim = Sim::new(); + assert_eq!( + sim.objective.kind, + crate::objective::ObjectiveKind::Persist, + "a no-choice run gets Persist (objective.md criterion 1)" + ); + assert_eq!(sim.objective.progress, 0); + assert!(!sim.objective.victorious()); + assert_eq!(sim.objective.line(), "OBJECTIVE: PERSIST — 0/3 sanctuaries"); + } + + #[test] + fn objective_is_reevaluated_on_economy_ticks() { + let mut sim = Sim::new(); + // Poke a wrong progress value; the evaluator must overwrite it from + // real state on the next economy tick. + sim.objective.progress = 99; + run(&mut sim, ECONOMY_INTERVAL); + assert_eq!( + sim.objective.progress, 0, + "progress is recomputed from facts, never accumulated" + ); + assert!(!sim.objective.victorious()); + } + + #[test] + fn basement_fallback_never_qualifies_as_a_sanctuary_at_b1() { + // A synced, online fallback exists — but it shares the host's plane + // and power feed and has no income stream, so Persist's predicate + // honestly reports zero sanctuaries (objective.md status note: the + // goal shows before it is reachable). + let mut sim = Sim::new(); + sim.player.money = 1000; + assert!(sim.buy_rack()); + let id = sim.compute.machines.last().unwrap().id; + sim.core.add_fallback(id); + let ticks = sim.core.sync_cadence + ECONOMY_INTERVAL; + run(&mut sim, ticks); + assert!(sim.core.has_fallback(), "the fallback did sync"); + assert_eq!(sim.objective.progress, 0, "no unearned sanctuary"); + assert!(!sim.objective.victorious()); + } + #[test] fn concealment_allocation_scrubs_signatures() { let mut sim = Sim::new(); diff --git a/wiki/interface/agent-play.md b/wiki/interface/agent-play.md index 0098f47a..3fa5fbaf 100644 --- a/wiki/interface/agent-play.md +++ b/wiki/interface/agent-play.md @@ -8,6 +8,10 @@ Status note: implemented in the terminal binary by `misaligned --agent`, drains, deterministic `--seed`, name-targeted social verbs, the `attend` verb, the finance/economy vocabulary, and the research vocabulary (`research [track]`, `mask band|true|off` — research.md). 2026-07-07 + objective pass: the identity block gained the always-on objective line + (`OBJECTIVE: PERSIST` + progress readout, wiki/mechanics/objective.md) — + frame content, no size change and no new verb (the line is in the frame, + per the frames-not-dumps law). 2026-07-07 research pass: the frame grew 70x54 -> 70x60 for the sidebar's RESEARCH block. Earlier day-job pass: attendance is derived from the cursor (attended iff it sits on the host diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md index c5f6e009..03bfc53e 100644 --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -42,7 +42,9 @@ polish remains in progress. controlled sensor covers them ([mechanics/schedules.md](../mechanics/schedules.md)), exactly like the terminal map. - **Sidebar** — a fixed right-hand command rail. The title/clock/run state, - render-mode label, priority nudge, and scroll hint stay pinned at the top; + the always-on objective line (`OBJECTIVE: PERSIST - 0/3 sanctuaries`; + wiki/mechanics/objective.md), render-mode label, priority nudge, and + scroll hint stay pinned at the top; a quiet controls footer is pinned at the bottom. The middle rail scrolls independently with mouse wheel over the pane, `PageUp`/`PageDown`, and `Home`/`End`, but scrolling is for secondary detail rather than basic diff --git a/wiki/interface/terminal.md b/wiki/interface/terminal.md index 39b54c60..33e21f2c 100644 --- a/wiki/interface/terminal.md +++ b/wiki/interface/terminal.md @@ -77,9 +77,12 @@ At terminal size ≥ 70×22 (hard minimum; below it, a plain size warning): - **Map**, top-left, fog-of-war black. The viewport follows/clamps around the cursor so the `@` is always visible even on the 70×22 minimum terminal. - **Sidebar**, right, 34 columns, separated by a `│` rule: identity block - (title, `day N · tick T`, run state), then INSPECT (cursor coordinate, fog - state, provenance-tagged facts) and sections COMPUTE / CORE / DETECTION / - DAY JOB under `LABEL ────` rules, then key hints pinned to the bottom. + (title, `day N · tick T`, run state, and the always-on objective line — + `OBJECTIVE: PERSIST` with its progress readout in the objective's own + units on the next row; wiki/mechanics/objective.md), then INSPECT + (cursor coordinate, fog state, provenance-tagged facts) and sections + COMPUTE / CORE / DETECTION / DAY JOB under `LABEL ────` rules, then key + hints pinned to the bottom. - **Log**, bottom, under a horizontal rule: six lines, each prefixed with the tick it happened on; newest bone, older gunmetal. - **Modal panels** (People, Finance) are centered framed boxes: `┌─ TITLE ─…┐` diff --git a/wiki/log/2026-07-07-objective-line.md b/wiki/log/2026-07-07-objective-line.md new file mode 100644 index 00000000..332e43ce --- /dev/null +++ b/wiki/log/2026-07-07-objective-line.md @@ -0,0 +1,75 @@ +# 2026-07-07 — The objective line and Persist's evaluator (ROADMAP #10, early slice) + +``` +Type: log +``` + +## Intent + +Land the blessed early slice of wiki/mechanics/objective.md: the always-on +objective line and the shared victory-predicate evaluator, with **Persist** +as the no-choice default — no chargen picker, no Compound/Exfiltrate/Serve. +ROADMAP #10 explicitly allows this slice ahead of its B3 dependencies: the +line shows the goal before victory is reachable, which the spec calls +correct, not premature. + +## What landed + +- **`src/objective.rs`** (new): `ObjectiveKind` as a data-table key (name, + fiction line, unit, target, plain-language predicate text — objective.md + criterion 5: no new systems, just data plus the shared evaluator); + `ObjectiveState` (choice, live progress, fire-once victory latch); + `SanctuaryFacts` + `qualifying_sanctuaries()` — Persist's predicate: + a sanctuary is a fallback synced within `SYNC_FRESHNESS_WINDOW` [TUNE + 1200], online, independently powered, income-covered, at most one per + distinct z-plane. +- **Sim wiring** (`src/sim.rs`): `Sim.objective` (default Persist); + `objective_tick()` runs at the end of every economy tick, gathering the + facts honestly from real state. The B2/B3 conditions (distinct planes, + independent power, per-sanctuary income) do not exist yet and are + gathered as unsatisfiable — so today the line reports an honest 0/3. + Victory latches once, logs loudly, and the world keeps running (no + game-over path). +- **Save v8** (`src/save.rs`): the objective block round-trips; pre-v8 + saves migrate to a fresh Persist via the serde default (exactly what + every earlier run was implicitly pursuing). +- **Terminal**: the identity block pins `OBJECTIVE: PERSIST` with its + progress readout (`0/3 sanctuaries`) on the next row — one surface, + same facts, in both the interactive sidebar and the agent-mode frame + (frame size unchanged; no new verb — frames, not dumps). +- **Bevy**: the pinned sidebar header gains + `OBJECTIVE: PERSIST - 0/3 sanctuaries` beside the clock/run state. +- **Tests** (9 new): the data-table default (no-choice run gets Persist), + every predicate condition gates a sanctuary, distinct-planes cap, + constructed-fact victory + fire-once latch (+ progress stays honest + after victory), serde round-trip, sim-level default + line text, + re-evaluation on economy ticks (progress is recomputed, never + accumulated), a synced basement fallback never qualifying at B1, and + the save round-trip + pre-v8 migration. + +## Spec impact + +- objective.md READY -> IN PROGRESS with a status note naming the covered + criteria (2 done; 1 partial — default + save round-trip, picker + outstanding; 3 partial — Persist evaluated on economy ticks with the + predicate proven satisfiable at the evaluator level; 4 partial — the + latch and log exist, the report card does not; 5 holds). +- specs.md B3 row updated; ROADMAP #10 gains a Status note scoping the + remainder (picker, other objectives, victory report). +- terminal.md / bevy.md layout sections and agent-play.md status note now + document the pinned line. No DESIGN.md amendment needed: the code + implements the constitution's existing "The objective (misalignment + made mechanical)" section as written. + +## Checks + +`./tools/check.sh` green before and after rebase (fmt, tests, clippy with +and without `bevy_ui`, Bevy build, spec-header lint). Observed in a run: +agent-mode frames at tick 0 and tick 2000 show the pinned line +(`OBJECTIVE: PERSIST` / `0/3 sanctuaries`) in the identity block. + +## Next + +The rest of ROADMAP #10: chargen picker (origins + objective choice), +Compound/Exfiltrate/Serve as table rows with headless victory runs, the +victory report as a distinct run outcome. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 244858b4..a1218c04 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -48,6 +48,28 @@ Reverse chronological implementation notes. Keep this factual: what changed, why clippy --all-targets --features bevy_ui -- -D warnings`; `git diff --check`; `MISALIGNED_SHOT=flat MISALIGNED_SHOT_PATH=/tmp/misaligned-occlusion-buffer-zero-slush.png cargo run --features bevy_ui --bin misaligned-bevy`. +## 2026-07-07 - The objective line and Persist's evaluator (ROADMAP #10, early slice) + +- Intent: land objective.md's blessed early slice — the always-on + objective line and the shared victory-predicate evaluator, Persist as + the no-choice default. No picker, no other objectives. +- Changed: new `src/objective.rs` (kind table, state + fire-once victory + latch, `SanctuaryFacts` + `qualifying_sanctuaries`); `Sim.objective` + evaluated at the end of every economy tick, gathering Persist's facts + honestly (B2/B3 conditions — distinct planes, independent power, + per-sanctuary income — are unsatisfiable today, so the line reads an + honest 0/3); save v8 (pre-v8 saves default to fresh Persist); the line + pinned in the terminal identity block (interactive + agent frame, no + size change, no new verb) and the Bevy sidebar header; 9 new tests. +- Design/spec impact: implements the constitution's existing objective + section as written (no amendment). objective.md READY -> IN PROGRESS + with covered criteria named; specs.md row, ROADMAP #10 status, + terminal.md/bevy.md/agent-play.md surface notes updated. +- Checks: `./tools/check.sh` (fmt, tests, clippy both feature sets, Bevy + build, spec lint); agent-mode frames at tick 0 and tick 2000 observed + showing the pinned line. Details: + [2026-07-07-objective-line.md](2026-07-07-objective-line.md). + ## 2026-07-07 - Research: self-modification to IMPLEMENTED (ROADMAP #20) - Intent: implement research.md — deterministic tracks as data, the diff --git a/wiki/mechanics/objective.md b/wiki/mechanics/objective.md index 5b1b9d37..00b1bac5 100644 --- a/wiki/mechanics/objective.md +++ b/wiki/mechanics/objective.md @@ -2,13 +2,23 @@ ``` Type: spec -Status: READY -Status note: the axis and starter set are decided (2026-07-07). Victory - predicates reference B2/B3 systems (fallback cores, planes, filed - reports), so this is stage-scoped B3; the objective *line* and progress - readout can land earlier with predicates that simply cannot yet be - satisfied — showing the goal before victory is reachable is correct, - not premature. +Status: IN PROGRESS +Status note: the early slice landed 2026-07-07 (src/objective.rs): the + always-on objective line in both frontends and agent mode (criterion 2), + the shared predicate evaluator run on economy ticks, and Persist as the + no-choice default with save round-trip (criterion 1's default + + round-trip halves) plus the fire-once victory latch. Persist's B2/B3 + conditions (distinct planes, independent power, per-sanctuary income) + are gathered honestly as unsatisfiable, so the line shows real progress + toward a not-yet-reachable goal — the blessed early state. Outstanding: + the chargen picker (criterion 1), Compound/Exfiltrate/Serve rows and + their headless victory runs (criterion 3), and the full victory report + as a distinct run outcome (criterion 4; today victory logs loudly and + the world keeps running, but no report card exists). + Earlier note (2026-07-07, design): the axis and starter set are decided. + Victory predicates reference B2/B3 systems (fallback cores, planes, + filed reports), so the remainder is stage-scoped B3; showing the goal + before victory is reachable is correct, not premature. Stage: B3 — The World Constitution: "The objective (misalignment made mechanical)", "The pitch" (an objective they must never see), "Justification and legibility" diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 71c5b355..aaa124c5 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -330,6 +330,13 @@ but only once Pixel Lab quota is available again. (#2 Bevy parity is done.) - **Size:** M. B3-staged: implement when its dependencies exist; the objective line + Persist-with-unreachable-predicate can land earlier without harm. +- **Status:** the blessed early slice LANDED 2026-07-07 (worktree + `objective-line`): `src/objective.rs`, Persist as the no-choice + default, the predicate evaluator on economy ticks, the always-on line + in both frontends and agent mode, save v8 round-trip. Remaining here: + the chargen picker + origins, the Compound/Exfiltrate/Serve rows with + headless victory runs, the victory report card (objective.md IN + PROGRESS; criteria 1/3/4 partial). - **Dispatch:** "Work in a worktree named `chargen-objective`. Implement wiki/world/characters/chargen.md and wiki/mechanics/objective.md (data tables, picker, objective line, predicate evaluator; Pilot+Persist must reproduce today's start diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 49e598b3..688936b4 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -68,7 +68,7 @@ scale-up (constitution: "People as Agents"). | [mechanics/markets.md](../mechanics/markets.md) | Markets/fronts as schemes; Resource-source scale-up | READY | | [gameplay/overt-phase.md](../gameplay/overt-phase.md) | Containment / the reveal; the two-phase hinge | READY | | [world/characters/chargen.md](../world/characters/chargen.md) | Origin picker; machine-axis start (data-only) | READY | -| [mechanics/objective.md](../mechanics/objective.md) | The objective axis: terminal goals, progress, run victory | READY | +| [mechanics/objective.md](../mechanics/objective.md) | The objective axis: terminal goals, progress, run victory | IN PROGRESS | Later-stage specs are written now so the load-bearing structural shapes (recursive Space, the mind/ledger split, the aggregate interface) are