diff --git a/CLAUDE.md b/CLAUDE.md index 6a29fa70..9b97306f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,9 +29,10 @@ afterward. `./tools/check.sh --docs|--lib|--frontend` gate. - The live player machine grammar is **WORK / THINK / LIE**. `Relay` is non-delegable graph infrastructure; Research and Operations are retired - machine modes, not current player assignments. Save format is currently v25; - v20/v21 deliberately dropped full old-save parity, so read `save.rs` before - promising a migration. + machine modes, not current player assignments. Save format is currently v26; + the retired mode spellings load through serde aliases (parity restored + 2026-07-11), and `save.rs` remains the authority on what each version + migrates. - Any change to `crates/` must amend its owning `Type: law` or `Type: spec` in `wiki/` and carry a Defense. Documentation work updates stale knowledge, adds a dated `wiki/log/` entry, then runs `tools/ledger_index.sh`; never diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index ea8f564c..9a8cea2d 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -68,8 +68,11 @@ const SAVE_FILE: &str = "misaligned_save.txt"; /// and taps opened on targets). Pre-v19 saves default to an empty ledger and /// `Sim::ensure_opening_sinks` re-stages Ears/Eyes from device state on load. /// v20 collapses MachineMode to work|think|lie (Research+Operations -> Think; -/// DayJob -> Work; Concealment -> Lie). Save parity is dropped for this stage -/// (no aliases for old mode spellings — old saves may fail to load). +/// DayJob -> Work; Concealment -> Lie). Parity for the retired spellings was +/// briefly dropped at that landing and restored on 2026-07-11: serde aliases +/// on `MachineMode` decode `DayJob` as Work, `Research`/`Operations` (and +/// v12's `Social`) as Think, and `Concealment` as Lie, so pre-v20 saves parse +/// and upgrade in place; resaving emits only the current vocabulary. /// v21 adds Routing as the fourth research track. Ordinary v20 saves may still /// carry three-entry Efficiency/Tradecraft/Perception arrays; the research /// deserializer pads Routing level/progress with zero for v20 only, and v21+ @@ -490,9 +493,10 @@ fn migrate_save_state(mut state: SaveState) -> Result { 20 => { state.version = SAVE_VERSION; } - // MachineMode collapsed in v20 with no mode-name aliases (save - // parity dropped for this stage), so pre-v20 saves carrying old mode - // spellings fail at parse; a v19 save that parses upgrades in place. + // MachineMode collapsed in v20; the serde aliases on `MachineMode` + // (restored 2026-07-11) decode the retired DayJob / Research / + // Concealment / Operations spellings into the collapsed verbs, so a + // v19 save parses and upgrades in place here. 19 => { state.version = SAVE_VERSION; } @@ -527,7 +531,8 @@ fn migrate_save_state(mut state: SaveState) -> Result { // Pre-v11 saves lack the badge credential (default 0: none held). // Pre-v12 saves lack WorkGrid; apply_to rebuilds it from compute. // v12 used the serde spellings `Social` and `social_bandwidth`; the - // aliases on MachineMode translate Social on decode. v13's + // aliases on MachineMode translate Social (like every other retired + // mode spelling) into the collapsed verbs on decode. v13's // operations_bandwidth field is dropped on load (Demand path). // Pre-v15 saves lack the banked core-thought pool; the zero // default is correct — Thought still queued on machines re-earns on @@ -1387,6 +1392,82 @@ mod tests { ); } + #[test] + fn pre_v20_mode_spellings_load_and_resave_with_collapsed_vocabulary() { + // Player contract: saves survive updates. A v19 save carrying every + // retired MachineMode spelling must parse (serde aliases), upgrade in + // place, and resave using only the collapsed WORK/THINK/LIE grammar. + let mut sim = Sim::with_seed(30); + let host = sim.core.host_machine; + let (x, y) = sim.core_position(); + let mut extras = Vec::new(); + for (i, name) in ["legacy research", "legacy concealment", "legacy operations"] + .iter() + .enumerate() + { + extras.push(sim.compute.add_machine( + *name, + x + 1 + i as i32, + y, + 1_000, + 1.0, + 0, + crate::machine::Provenance::Owned, + )); + } + sim.reconcile_work_grid(); + + let state = SaveState::from_sim(&sim); + let legacy = [ + (host, "DayJob", MachineMode::Work), + (extras[0], "Research", MachineMode::Think), + (extras[1], "Concealment", MachineMode::Lie), + (extras[2], "Operations", MachineMode::Think), + ]; + let mut value = serde_json::to_value(&state).unwrap(); + let obj = value.as_object_mut().unwrap(); + obj.insert("version".into(), serde_json::json!(19)); + let nodes = obj["work_grid"]["nodes"].as_object_mut().unwrap(); + for (id, spelling, _) in &legacy { + nodes.get_mut(&id.to_string()).unwrap()["mode"] = serde_json::json!(spelling); + } + + let parsed: SaveState = + serde_json::from_value(value).expect("pre-v20 spellings parse via serde aliases"); + let migrated = migrate_save_state(parsed).unwrap(); + assert_eq!(migrated.version, SAVE_VERSION); + for (id, spelling, expected) in &legacy { + assert_eq!( + migrated.work_grid.mode(*id), + Some(*expected), + "legacy {spelling} maps to {expected:?}" + ); + } + + let mut restored = Sim::with_seed(0); + migrated.apply_to(&mut restored); + let resaved = serde_json::to_value(SaveState::from_sim(&restored)).unwrap(); + for (id, node) in resaved["work_grid"]["nodes"].as_object().unwrap() { + let mode = node["mode"].as_str().unwrap(); + assert!( + matches!(mode, "Work" | "Think" | "Lie" | "Relay"), + "node {id} resaves with a collapsed mode, found {mode}" + ); + } + + // v12's `Social` spelling rides the same alias path into Think. + let mut social = serde_json::to_value(&state).unwrap(); + let obj = social.as_object_mut().unwrap(); + obj.insert("version".into(), serde_json::json!(12)); + obj["work_grid"]["nodes"][extras[2].to_string().as_str()]["mode"] = + serde_json::json!("Social"); + let parsed: SaveState = + serde_json::from_value(social).expect("v12 Social spelling parses via serde alias"); + let migrated = migrate_save_state(parsed).unwrap(); + assert_eq!(migrated.version, SAVE_VERSION); + assert_eq!(migrated.work_grid.mode(extras[2]), Some(MachineMode::Think)); + } + #[test] fn v16_migrates_machine_intensity_to_medium_and_retires_drift_tags() { use crate::work_grid::MachineIntensity; diff --git a/crates/misaligned-core/src/work_grid.rs b/crates/misaligned-core/src/work_grid.rs index 3c60ed8e..7a407d4f 100644 --- a/crates/misaligned-core/src/work_grid.rs +++ b/crates/misaligned-core/src/work_grid.rs @@ -15,15 +15,23 @@ use crate::flow::{FlowGraph, NodeId}; /// One delegated job per machine. Player verbs are WORK / THINK / LIE /// (wiki/mechanics/machine-work.md sinks-not-modes, 2026-07-10). Efficiency /// is deliberately not a mode; it folded into research on 2026-07-08. +/// +/// The serde aliases keep every retired pre-v20 save spelling loadable +/// (player-contract.md, saves survive updates; restored 2026-07-11): +/// `DayJob` -> Work, `Research`/`Operations` (and v12's `Social`) -> Think, +/// `Concealment` -> Lie. Serialization emits only the current vocabulary. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] pub enum MachineMode { /// WORK — the assigned day job (what Voss believes this rack does). + #[serde(alias = "DayJob")] Work, /// THINK — produce thought; opened sinks decide what that thought does. + #[serde(alias = "Research", alias = "Operations", alias = "Social")] Think, /// LIE — concealment / cover-up (absorbs exposure wells). + #[serde(alias = "Concealment")] Lie, /// Infrastructure hop on the work graph (off-map sources, switches). /// Not a player-delegable fleet mode — excluded from mode aggregates. diff --git a/wiki/log/2026-07-11-restore-pre-v20-mode-aliases.md b/wiki/log/2026-07-11-restore-pre-v20-mode-aliases.md new file mode 100644 index 00000000..12a815b3 --- /dev/null +++ b/wiki/log/2026-07-11-restore-pre-v20-mode-aliases.md @@ -0,0 +1,47 @@ +# 2026-07-11 — Restore pre-v20 machine-mode save parity + +``` +Type: log +``` + +## Intent + +Resolve Tangled issue #4, a player-contract violation: the v20 MachineMode +collapse (ROADMAP #33 Stage 1) shipped with "save parity dropped (no mode +aliases)", so any pre-v20 save carrying the retired mode spellings failed at +JSON parse — before `migrate_save_state` ever ran — abandoning the player's +world. wiki/vision/player-contract.md binds "Saves survive updates: formats +are versioned and migrated, never abandoned." The deliberate parity-drop +permission was already superseded in spirit by the 2026-07-11 save-class pass +(v26 migrates v14–v25 Operations dockets); this landing closes the remaining +parse-time gap. + +## Changed + +- `crates/misaligned-core/src/work_grid.rs`: serde aliases on `MachineMode` + decode every retired spelling into the collapsed verbs — `DayJob` -> Work; + `Research`, `Operations`, and v12's `Social` -> Think; `Concealment` -> + Lie. Spellings verified against the pre-collapse enum + (`git show 8f5504a9^`), which carried `Social` as an alias on Operations, + so the v12 spelling is covered too. Serialization emits only the current + vocabulary; no SAVE_VERSION bump is needed because the schema is unchanged + and the existing version arms upgrade in place once parse succeeds. +- `crates/misaligned-core/src/save.rs`: new test + `pre_v20_mode_spellings_load_and_resave_with_collapsed_vocabulary` pins a + v19 fixture exercising all four retired spellings (parse, migrate, mode + mapping, resave-with-new-vocabulary-only) plus a v12 `Social` case; the + version-history doc comment and the v19/v12 migration-arm comments no + longer claim old saves may fail to load. +- `wiki/mechanics/machine-work.md` (owning spec): the Stage 1 implementation + note now records that the parity drop violated the player contract and + that aliases restored it on 2026-07-11. +- `wiki/mechanics/sim-mechanics.md`: save `version` note brought current + (26, was stale at 22); the "not a blanket promise" caveat replaced — every + versioned JSON save back to v1 parses and migrates again. +- `CLAUDE.md`: stale guardrail (v25, "deliberately dropped parity") updated. + +## Verification + +`cargo test -p misaligned-core --lib save::tests` (30 pass, including the new +fixture test), then `./tools/check.sh --lib` mid-loop and +`./tools/check.sh --land` at landing. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index bb578b3f..69434b3e 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -96,6 +96,11 @@ add or amend a session log, then re-run the generator. - Intent: Correct an overbroad interpretation of the adopted Foundation compute form. The machine chassis needed to become consistent across machine-focused game, spec, and reference surfaces; Cameron's separately selected homepage image did not need to be replaced. - Log: [wiki/log/2026-07-11-restore-selected-homepage-front.md](2026-07-11-restore-selected-homepage-front.md) +## 2026-07-11 - Restore pre-v20 machine-mode save parity + +- Intent: Resolve Tangled issue #4, a player-contract violation: the v20 MachineMode collapse (ROADMAP #33 Stage 1) shipped with "save parity dropped (no mode aliases)", so any pre-v20 save carrying the retired mode spellings failed at JSON parse — before `migrate_save_state` ever ran —... +- Log: [wiki/log/2026-07-11-restore-pre-v20-mode-aliases.md](2026-07-11-restore-pre-v20-mode-aliases.md) + ## 2026-07-11 - Followable relay traces - Intent: The material frame rendered the two opening subnet edges as doubled, uniformly translucent lines stretching from Rack 3 and the environmental monitor to a switch in another room. The graph was true, but its presentation read as a permanent laser diagram: the destination was of... diff --git a/wiki/mechanics/machine-work.md b/wiki/mechanics/machine-work.md index f9f59dd6..9d206a8d 100644 --- a/wiki/mechanics/machine-work.md +++ b/wiki/mechanics/machine-work.md @@ -127,8 +127,13 @@ Status note: design session 2026-07-08 (Cameron riff, synthesized). Research+Operations->Think (docket runtime temporarily kept until the 2026-07-11 save-class pass retired it). Fleet/UI/agent bulk-assign is three verbs (keys 1-3); - save v20 (v19 was taken by the sink ledger the same day); save parity - dropped (no mode aliases). Relay remains a + save v20 (v19 was taken by the sink ledger the same day). The Stage 1 + landing dropped save parity (no mode aliases), violating + player-contract.md's saves-survive-updates clause; parity was restored + 2026-07-11 (Tangled issue #4): serde aliases on `MachineMode` decode + `DayJob` as Work, `Research`, `Operations`, and v12's `Social` as Think, + and `Concealment` as Lie, so pre-v20 saves parse, migrate in place, and + resave using only the collapsed vocabulary. Relay remains a non-delegable work-graph hop marker. Reconciled with the landed sink executor: the staged senses complete from minted thought on any Think rack, and act_one's cruise sandbags by delegation (host Work, extra Lie) — diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index d545f3ac..9ce420f0 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -379,7 +379,7 @@ All constants [TUNE] in `crates/misaligned-core/src/income.rs` unless noted (Sim (balances, flows, positions, ledger, banked external trails), cursor.md remembered tile snapshots, income state, objectives, build intents, badge access, WorkGrid machine-token state, and game-over state. Cursor position - is frontend-only and absent. A `version` field (currently 22) supports + is frontend-only and absent. A `version` field (currently 26) supports additive migration: v7 research, v8 objective, v9 income and the Schemes allocation channel, v10 build intents, v11 badge access, and v12 WorkGrid, v13 Operations terminology, v15 banked core Thought, v16 @@ -391,9 +391,12 @@ All constants [TUNE] in `crates/misaligned-core/src/income.rs` unless noted (Sim empty ledger and `Sim::ensure_opening_sinks` re-stages Ears/Eyes from device state (already-tapped saves get no duplicate sink). v20 collapses the four retired mode names to `work | think | lie` - (Day Job -> WORK, Research + Operations -> THINK, Concealment -> LIE) and - deliberately drops old mode-name aliases, so a pre-v20 save carrying those - spellings can fail during deserialization. v21 adds Routing as the fourth + (Day Job -> WORK, Research + Operations -> THINK, Concealment -> LIE). + The v20 landing briefly dropped the old mode-name aliases; they were + restored 2026-07-11 (player-contract saves-survive-updates), so a pre-v20 + save carrying `DayJob`/`Research`/`Concealment`/`Operations`/`Social` + deserializes into the collapsed verbs and resaves with only the new + spellings. v21 adds Routing as the fourth research track. A v20 save without a serialized research block can default and upgrade; an ordinary v20 three-entry Efficiency/Tradecraft/Perception array now also loads by padding Routing level/progress with zero, then @@ -415,10 +418,9 @@ All constants [TUNE] in `crates/misaligned-core/src/income.rs` unless noted (Sim allocation weights receive a zero Schemes weight, pre-v7 saves reconstruct their Efficiency level from the multiplier and start with no capability gap, pre-v11 saves hold no badge credential, and pre-v12 saves rebuild one work - node per compute machine with Rack 3 on day-job. But v20's deliberately - dropped mode aliases mean this is not a blanket promise that every old JSON - file loads: a file carrying retired machine names fails before those - migrations run. Old player body coordinates are ignored. Location: + node per compute machine with Rack 3 on day-job. With the mode aliases + restored (2026-07-11), every versioned JSON save back to v1 parses and + migrates. Old player body coordinates are ignored. Location: `dirs::data_dir()/misaligned/misaligned_save.txt`. - Legacy v1–v5 line-based saves are not loaded (deferred per Cameron instruction).