diff --git a/AGENTS.md b/AGENTS.md index 4d9371e0..ee8b12a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,9 +30,11 @@ statuses afterward. - 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. The save schema moves - quickly: read `SAVE_VERSION`, its version history, and - `migrate_save_state` in `crates/misaligned-core/src/save.rs` before making - any compatibility claim. Do not freeze a version number in this doorway. + quickly: read `SAVE_VERSION` and the current-schema load gate in + `crates/misaligned-core/src/save.rs` before making any compatibility claim. + During pre-release only the exact current version loads; the deleted + migration ladder lives in git history. Do not freeze a version number in + this doorway. - 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/README.md b/README.md index 04a8c1f6..713efc2a 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,10 @@ objective continues. Continuous witness/narration is implemented and serves as the legibility gate for every new player-facing system. Machine work and compute are implemented: WORK / THINK / LIE, physical production/consumption/absorption, researched Routing, and target-local -Thought reservoirs all run through one flow substrate. The legacy Operations -docket executor survives only as v14-v25 save input. People now project as -schedule-bound carriers; builds and physical asset tasks travel with their -selected person and act only on arrival. Remaining B1 work includes routed +Thought reservoirs all run through one flow substrate. The retired Operations +docket executor is absent from both runtime and the current save schema. People +now project as schedule-bound carriers; builds and physical asset tasks travel +with their selected person and act only on arrival. Remaining B1 work includes routed human evidence, LIE interdiction, gauntlet cover channels, the dark opening, and digital/real canvas polish; core completion waits on rollback. B2/B3 systems are specified ahead but remain staged. Under the corpus's no-dead-code diff --git a/crates/misaligned-core/src/intel.rs b/crates/misaligned-core/src/intel.rs index a1e25c4c..fe1ac865 100644 --- a/crates/misaligned-core/src/intel.rs +++ b/crates/misaligned-core/src/intel.rs @@ -1016,14 +1016,6 @@ impl IntelPolicyLedger { } } -/// Legacy v23 per-person watch policy. Current saves carry one global -/// `auto_review_recordings` flag; this shape remains readable for migration. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct LegacyIntelWatch { - pub person: u8, - pub enabled: bool, -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/misaligned-core/src/person.rs b/crates/misaligned-core/src/person.rs index 16bc51c6..45d05623 100644 --- a/crates/misaligned-core/src/person.rs +++ b/crates/misaligned-core/src/person.rs @@ -597,26 +597,6 @@ impl People { self.people.iter().find(|p| p.id == id) } - /// Add the role characteristic to saves written before reusable plot - /// matching existed. Only the fixed B1 cast needs this compatibility - /// bridge; generated/later humans serialize their authored role directly. - pub(crate) fn restore_legacy_roles(&mut self) { - use PersonRole::*; - for person in &mut self.people { - if person.role != Unassigned { - continue; - } - person.role = match person.id { - 0 => Custodian, - 1 => NetworkAdministrator, - 2 => SecurityObserver, - 3 => FacilitiesManager, - 4 => HandlerSupervisor, - _ => Unassigned, - }; - } - } - fn get_mut(&mut self, id: u8) -> Option<&mut Person> { self.people.iter_mut().find(|p| p.id == id) } diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 2a54d8b2..26e73402 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -39,8 +39,9 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// Save format version. v33 adds the handler-only SuppressLogs asset task to -/// persisted Thought-sink effects. Bump for every schema change; pre-release -/// policy deliberately requires a fresh run instead of compatibility shims. +/// persisted Thought-sink effects. Bump for every schema change; during +/// pre-release, old development state is refused instead of carried through +/// compatibility shims. pub const SAVE_VERSION: u32 = 33; fn save_dir() -> PathBuf { @@ -62,7 +63,8 @@ pub fn save_exists() -> bool { /// Serializable game state snapshot (full B1 round-trip). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SaveState { - /// Save format version for future migration. + /// Save format version for the current-schema load gate and future + /// release-era migration. pub version: u32, pub money: i32, #[serde(default)] @@ -116,10 +118,6 @@ pub struct SaveState { /// Latest filing reports read by aggregate observers. #[serde(default)] pub filing_levels: HashMap, - /// Compatibility-only v29 root auto-review flag. v30 writes the explicit - /// root policy in `intel_policies` instead. - #[serde(default, skip_serializing)] - pub auto_review_recordings: bool, #[serde(default = "default_next_intel_id")] pub next_intel_id: u64, /// Remembered tile snapshots (cursor.md); cursor position itself remains @@ -135,7 +133,7 @@ pub struct SaveState { /// Thought (compute units) delivered to the current core sink but not yet /// consumed by an economy pulse (machine-work.md: research feeds on /// arrival while the later local-sink behavior remains open). - #[serde(default, alias = "banked_core_knowledge")] + #[serde(default)] pub banked_core_thought: f32, /// The run objective: choice, progress, victory latch /// (wiki/mechanics/objective.md criterion 1). @@ -177,8 +175,7 @@ pub struct SaveState { #[serde(default)] pub institutional_ledger: InstitutionalLedger, pub package_cover: bool, - /// What this run was built for (chargen.md). Pre-v28 saves lack the field - /// and default to Pilot, the identity origin. + /// What this run was built for (chargen.md). #[serde(default)] pub origin: crate::origin::Origin, } @@ -221,7 +218,6 @@ impl SaveState { message_schedule: sim.message_schedule.clone(), next_message_id: sim.next_message_id, filing_levels: sim.filing_levels.clone(), - auto_review_recordings: false, next_intel_id: sim.next_intel_id, remembered: sim.remembered.values().copied().collect(), research: sim.research.clone(), @@ -267,7 +263,6 @@ impl SaveState { sim.detection = self.detection.clone(); sim.dayjob = self.dayjob.clone(); sim.people = self.people.clone(); - sim.people.restore_legacy_roles(); sim.persona_world = self.persona_world.clone(); sim.persona_mind = self.persona_mind.clone(); sim.reach = self.reach.clone(); @@ -302,12 +297,9 @@ impl SaveState { sim.origin = self.origin; sim.recompute_derived(); sim.reconcile_work_grid(); - // Pre-v19 saves carry no ledger: re-stage the opening senses from - // device state (idempotent; a fired Ears in the ledger is respected). + // Re-establish current cross-ledger invariants before exposing the + // loaded state. These operations are idempotent over a valid save. sim.ensure_opening_sinks(); - // Saves written before device subscriptions gained standing upkeep - // have the subscriber record but no persistent sink. Repair those - // taps in place; owned devices remain drain-free. sim.reconcile_device_tap_sinks(); sim.reconcile_auto_review(); sim.recompute_senses(); @@ -612,7 +604,7 @@ mod tests { let checkpoint_bytes = canonical_state_bytes(&checkpoint); let encoded = serde_json::to_vec(&checkpoint).expect("checkpoint encodes"); let decoded: SaveState = serde_json::from_slice(&encoded).expect("checkpoint decodes"); - let decoded = validate_current_save(decoded).expect("current checkpoint migrates"); + let decoded = validate_current_save(decoded).expect("current checkpoint validates"); let mut resumed = Sim::with_seed(0); decoded.apply_to(&mut resumed); @@ -964,8 +956,8 @@ mod tests { state.plot_runs.push(PlotRun::new(&plot, 1, 0)); state.people.people[1].role = PersonRole::Custodian; - let migrated = validate_current_save(state).unwrap(); - assert_eq!(migrated.plot_runs[0].target, 1); + let validated = validate_current_save(state).unwrap(); + assert_eq!(validated.plot_runs[0].target, 1); } #[test] @@ -1085,15 +1077,21 @@ mod tests { fn old_version_saves_are_refused_legibly() { // The pre-release rider (player-contract, 2026-07-16): no // migration ladder. Any non-current version refuses with a message - // that names the policy and promises the file survives. + // that names the policy, promises the file survives, and accurately + // says that the caller's active run is not replaced. for version in [1, 20, 30, 31, 999] { let err = parse_save(&format!("{{\"version\":{version}}}")).unwrap_err(); assert!( err.contains("older development build") && err.contains("do not migrate") + && err.contains("current run is unchanged") && err.contains("remain on disk"), "v{version} refusal must be legible: {err}" ); + assert!( + !err.contains("Starting fresh"), + "a failed load must not promise a reset the caller does not perform: {err}" + ); } // A current-version save round-trips through the same gate. let sim = Sim::new(); diff --git a/crates/misaligned-core/src/work_grid.rs b/crates/misaligned-core/src/work_grid.rs index 3dd5211f..1c3fc058 100644 --- a/crates/misaligned-core/src/work_grid.rs +++ b/crates/misaligned-core/src/work_grid.rs @@ -15,10 +15,8 @@ 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. -/// -/// (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. +/// Current saves serialize only this vocabulary; retired mode spellings are +/// not accepted by the pre-release current-schema load path. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] diff --git a/tools/corpus_engine.py b/tools/corpus_engine.py index 92f0dcfa..fbaf28b5 100755 --- a/tools/corpus_engine.py +++ b/tools/corpus_engine.py @@ -316,10 +316,23 @@ class Engine: "DIGITAL is home and F3 flips to REAL" ) - def validate_doorway_save_version(self) -> None: - """The CLAUDE.md doorway's 'Save format is currently vN' claim must - match save.rs's SAVE_VERSION. Stale twice (v26->v27 on 2026-07-11, - v29->v31 on 2026-07-15); recurrence promotes to the gate.""" + def validate_doorway_save_policy(self) -> None: + """Keep both agent doorways on the live save policy. + + CLAUDE.md's numeric claim must match SAVE_VERSION. Neither doorway may + direct agents to the migration entry point deleted by the pre-release + current-version-only rider. + """ + for name in ("AGENTS.md", "CLAUDE.md"): + doorway = self.root / name + if doorway.exists() and re.search( + r"\bmigrate_save_state\b", doorway.read_text() + ): + self.bad( + f"{name} doorway references deleted save migration entry point " + "migrate_save_state; pre-release loads only the exact current version" + ) + save_rs = self.root / "crates/misaligned-core/src/save.rs" doorway = self.root / "CLAUDE.md" if not save_rs.exists() or not doorway.exists(): @@ -339,7 +352,7 @@ class Engine: ) def run_corpus(self) -> int: - self.validate_doorway_save_version() + self.validate_doorway_save_policy() for path in self.wiki_md_files(): rel = path.relative_to(self.root).as_posix() if rel == "wiki/SUMMARY.md": diff --git a/tools/test_corpus_engine.sh b/tools/test_corpus_engine.sh index b46ce697..b71cf2b5 100755 --- a/tools/test_corpus_engine.sh +++ b/tools/test_corpus_engine.sh @@ -280,6 +280,44 @@ echo "$out" | grep -q 'must declare exactly one supported Type' || { fail=1 } +echo "=== fixture: agent doorway save policy ===" +root=$tmp/doorway-save-policy +setup_base "$root" +mkdir -p "$root/crates/misaligned-core/src" +cat > "$root/crates/misaligned-core/src/save.rs" <<'EOF' +pub const SAVE_VERSION: u32 = 33; +EOF +cat > "$root/CLAUDE.md" <<'EOF' +Save format is currently v33; only the current version loads. +EOF +cat > "$root/AGENTS.md" <<'EOF' +Read SAVE_VERSION and migrate_save_state before making compatibility claims. +EOF +assert_fails doorway-save-deleted-symbol --root "$root" --corpus +out=$(python3 "$engine" --root "$root" --corpus 2>&1 || true) +echo "$out" | grep -q 'AGENTS.md doorway references deleted save migration entry point' || { + echo "FAIL: deleted AGENTS.md migration-entry message missing" + echo "$out" + fail=1 +} +cat > "$root/AGENTS.md" <<'EOF' +Read SAVE_VERSION and the current-schema load gate before making compatibility claims. +EOF +cat > "$root/CLAUDE.md" <<'EOF' +Save format is currently v32; only the current version loads. +EOF +assert_fails doorway-save-version --root "$root" --corpus +out=$(python3 "$engine" --root "$root" --corpus 2>&1 || true) +echo "$out" | grep -q 'CLAUDE.md doorway says save format v32 but save.rs SAVE_VERSION is 33' || { + echo "FAIL: stale CLAUDE.md save-version message missing" + echo "$out" + fail=1 +} +cat > "$root/CLAUDE.md" <<'EOF' +Save format is currently v33; only the current version loads. +EOF +assert_ok doorway-save-policy --root "$root" --corpus + echo "=== fixture: generated page without owner ===" root=$tmp/generated-owner setup_base "$root" diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index c0f63af3..0129191d 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -32,7 +32,7 @@ fiction. Spec status lives in | Feel floor (rails / pads / build beam) | Live (#37) | | Foundation hall territory (Dana + Priya + Marcus + local LIE foothold) | Live — row control persists; foreign racks remain unavailable compute | | Context menu (`available_actions`) | Live | -| Save/load (serde JSON, versioned) | Live — v29 adds the run origin (chargen.md; pre-v29 saves default to Pilot); v28 replaces the global persona with named persona instances (personas.md); v27 adds persisted carried asset-task packets; v14-v25 Operations dockets migrate into exact-remaining-work Thought reservoirs, v24 restores the pooled review policy, and v25 restores durable person roles/account binding | +| Save/load (serde JSON, versioned) | Live — during pre-release only the exact current `SAVE_VERSION` loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves persist run origin, named personas, carried asset-task packets, recursive intel custody, committed build routes, and handler log-suppression work; retired migration inputs live only in git history. | | Terminal frontend (crossterm) + agent mode | First-class | | Bevy frontend (DIGITAL flat sensorium default; REAL material dialect) | Live — consumes sim-authored machine-work motion | diff --git a/wiki/log/2026-07-18-save-policy-cleanup.md b/wiki/log/2026-07-18-save-policy-cleanup.md new file mode 100644 index 00000000..90e3c3a9 --- /dev/null +++ b/wiki/log/2026-07-18-save-policy-cleanup.md @@ -0,0 +1,55 @@ +# Save policy matches the load path + +``` +Type: log +``` + +The 2026-07-16 pre-release rider deleted the v1-v30 migration ladder, but a +merge audit found that the surrounding tree still described and carried parts +of the old contract. `AGENTS.md` directed agents to the deleted +`migrate_save_state`; the README, current-build inventory, and three current +simulation/core surfaces still promised legacy migration; source comments +said retired mode spellings loaded; and four compatibility-only shapes remained +reachable in the current schema path. + +This pass makes the boundary literal. Only the exact `SAVE_VERSION` reaches +full deserialization and current-state validation. A failed old-version load +does not replace the caller's active `Sim`; it also does not write the save or +its rotated backup. The earlier save-rider log's fresh-launch observation was +an application starting with a fresh `Sim` and then retaining it after the +load failed, not a reset performed by `load_game`. The legible-refusal test now +pins both the `current run is unchanged` sentence and the absence of the old +`Starting fresh` promise. + +The removed compatibility remnants were the skipped +`auto_review_recordings` root flag, the `banked_core_knowledge` serde alias, +`LegacyIntelWatch`, and the fixed-cast `restore_legacy_roles` repair. All four +fed only the deleted development-save ladder. Current save serialization, +atomic replacement, backup rotation, current-state validation, and the +idempotent cross-ledger reconstruction performed after a valid load remain. + +The recurrent doorway defense now checks both agent doorways. It retains the +existing `CLAUDE.md` numeric equality check against `SAVE_VERSION` and rejects +either `CLAUDE.md` or `AGENTS.md` if it points at the deleted migration entry +point. The fixture proves stale AGENTS wording, stale CLAUDE version text, and +the clean two-doorway case independently. + +Observed focused defenses: + +```sh +cargo test -p misaligned-core save::tests +./tools/test_corpus_engine.sh +python3 tools/corpus_engine.py --corpus +``` + +The focused current-save suite passed, including disk round-trip, atomic +failure, one-generation backup rotation, replay/resume fingerprint, exact +route validation, and old-version refusal. The complete fixture suite and live +corpus gate passed; the proportional documentation and library gates passed as +well. + +Defense: `wiki/vision/player-contract.md` permits development saves to break +only through a legible pre-release policy, while `wiki/mechanics/core.md` and +`wiki/mechanics/sim-mechanics.md` own the actual load and persistence contract. +Deleting the ladder without deleting its instructions and compatibility +surface left two truths; this repair gives the current path one. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 1ae6297e..52983fd5 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-07-18 - Save policy matches the load path + +- Intent: (see session log) +- Log: [wiki/log/2026-07-18-save-policy-cleanup.md](2026-07-18-save-policy-cleanup.md) + ## 2026-07-17 - Voss can suppress one flagged job log - Intent: (see session log) diff --git a/wiki/mechanics/core.md b/wiki/mechanics/core.md index 1d87055d..ec6293f8 100644 --- a/wiki/mechanics/core.md +++ b/wiki/mechanics/core.md @@ -3,12 +3,14 @@ ``` Type: spec Status: IN PROGRESS -Status note: 2026-07-16 — the pre-release save rider was collected: - load_game refuses any version but SAVE_VERSION with a legible message - (old file and .bak survive; the run starts fresh), and the v1-v30 - migration ladder, its serde aliases, and their tests were deleted (git - history keeps them for the release-era ladder). Atomic writes, backup - rotation, and current-version consistency checks are unchanged. +Status note: 2026-07-18 — the current-version-only rider is internally + consistent: load_game refuses any version but SAVE_VERSION before full + deserialization; the caller's active run and both on-disk generations stay + unchanged. Four orphaned compatibility shims left after the ladder deletion + are gone, and the corpus gate now rejects doorway references to the deleted + migration entry point. Git history retains the release-era ladder seed. + Atomic writes, backup rotation, and current-version consistency checks are + unchanged. 2026-07-08 audit: criteria 2-5 hold (overhead_charged_before_ allocation, migration_takes_time/emits_signatures + interruption via resolve_interrupted, save round-trip, both sidebars). diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index 919ac720..0af0bdbd 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -30,8 +30,8 @@ clause (see wiki/log/2026-07-05-demolition.md). non-delegable infrastructure. THINK has one output: Thought. Produced Thought routes nearest-first to open target/carrier sinks (reservoirs and taps); the core's passive draw takes only what reaches it. The live - Operations docket executor is gone (2026-07-11) — v14–v25 dockets migrate - into exact-remaining-work Thought reservoirs on load; the compatibility + Operations docket executor and its compatibility input are gone; current + saves carry target-local Thought sinks directly, and the compatibility bandwidth pool is gone. - Buy (slush → rack), steal (salvage dead equipment), optimize (research → efficiency levels at ~1.15× per level; `Compute` keeps only the applied @@ -220,8 +220,8 @@ clause (see wiki/log/2026-07-05-demolition.md). - Every player-authored payload is typed on a target/carrier-local Thought reservoir or persistent tap. Open effects suppress duplicates and semantic conflicts; a one-shot applies its effect and transient fire readout, then is - reaped without reusing ids. The retired docket types survive only as v14-v25 - deserialization input in `save.rs`. + reaped without reusing ids. The retired docket types survive only in git + history; they are absent from the current schema and `save.rs` load path. - Thought sinks (2026-07-10, machine-work.md thought flow; interface/thought-fluid.md render): `SinkLedger` in `crates/misaligned-core/src/sinks.rs` @@ -399,8 +399,8 @@ All constants [TUNE] in `crates/misaligned-core/src/income.rs` unless noted (Sim - **Only the current save version loads (pre-release rider, 2026-07-16).** `load_game` probes the version field first: anything but `SAVE_VERSION` is refused with a message that names the policy and promises the old - file (and its `.bak`) stays on disk; the caller starts a fresh run. The - v1-v30 migration ladder, its serde aliases for retired spellings, and + file (and its `.bak`) stays on disk; the caller's active run remains + unchanged. The v1-v30 migration ladder, its serde aliases for retired spellings, and their tests were deleted the same day — git history keeps them as the seed of the release-era ladder the player contract's continuity clause will require from the first public release. `save.rs` remains the