From 509238c0c731439ace67fa4a1f9bf6ad3760da87 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 7 Jul 2026 22:46:34 -0700 Subject: [PATCH] Objective line and Persist evaluator: objective.md early slice (ROADMAP #10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The always-on objective line and the shared victory-predicate evaluator, with Persist as the no-choice default. New src/objective.rs: the kind data table (name, fiction, unit, target, plain-language predicate), ObjectiveState with a fire-once victory latch, and Persist's evaluator (SanctuaryFacts + qualifying_sanctuaries: fresh sync, online, independent power, income coverage, at most one sanctuary per distinct z-plane). Sim.objective is re-evaluated from real state at the end of every economy tick; the B2/B3 conditions (planes, independent power, per-sanctuary income) are gathered honestly as unsatisfiable until those systems exist, so the line reads an honest 0/3 today. Save v8 round-trips the objective; pre-v8 saves migrate to a fresh Persist. The line is pinned in the terminal identity block (interactive sidebar and agent-mode frame — no size change, no new verb) and the Bevy sidebar header. Nine new tests cover the default, each predicate condition, the distinct-planes cap, constructed-fact victory with the latch firing exactly once, serde/save round-trips, migration, and re-evaluation on economy ticks. objective.md READY -> IN PROGRESS with the covered criteria named (criterion 2 done; 1 and 3 partial; picker, other objectives, and the victory report card remain). specs.md row, ROADMAP #10 status, and the terminal/bevy/agent-play interface pages updated in the same commit. Defense: implements DESIGN.md's "The objective (misalignment made mechanical)" section as written, no amendment needed: the objective is concrete and legible from tick one (the line sits on screen in both frontends with progress in its own units — the legibility law applied to your own goal first), it is data plus the shared evaluator (no new system or rule branch), and victory-when-reached is latched by the sim like any other rule while the world keeps running. Showing Persist's goal while its predicate is still unreachable is the spec's own blessed early state: every fact on the line is earned (fallback sync state and plane/power/income facts audited from real sim state), and no fact is inflated to fake progress. --- src/bin/bevy.rs | 8 +- src/bin/terminal/agent.rs | 8 + src/bin/terminal/ui.rs | 15 ++ src/lib.rs | 1 + src/objective.rs | 250 ++++++++++++++++++++++++++ src/save.rs | 44 ++++- src/sim.rs | 91 ++++++++++ wiki/interface/agent-play.md | 4 + wiki/interface/bevy.md | 4 +- wiki/interface/terminal.md | 9 +- wiki/log/2026-07-07-objective-line.md | 75 ++++++++ wiki/log/DEVLOG.md | 22 +++ wiki/mechanics/objective.md | 24 ++- wiki/process/ROADMAP.md | 7 + wiki/process/specs.md | 2 +- 15 files changed, 548 insertions(+), 16 deletions(-) create mode 100644 src/objective.rs create mode 100644 wiki/log/2026-07-07-objective-line.md 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 -- 2.51.2