Add save-slice-backed runtime fixtures

This commit is contained in:
Jan Petykiewicz 2026-04-14 20:51:27 -07:00
commit 8ca65cbbfb
12 changed files with 974 additions and 47 deletions

View file

@ -16,13 +16,15 @@ use rrt_model::{
};
use rrt_runtime::{
CAMPAIGN_SCENARIO_COUNT, CampaignExeInspectionReport, OBSERVED_CAMPAIGN_SCENARIO_NAMES,
Pk4ExtractionReport, Pk4InspectionReport, RuntimeSnapshotDocument, RuntimeSnapshotSource,
RuntimeSummary, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock, SmpInspectionReport,
SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary, WinInspectionReport,
execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file, inspect_pk4_file,
inspect_smp_file, inspect_win_file, load_runtime_snapshot_document, load_runtime_state_import,
load_save_slice_file, project_save_slice_to_runtime_state_import,
save_runtime_snapshot_document, validate_runtime_snapshot_document,
Pk4ExtractionReport, Pk4InspectionReport, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeSnapshotDocument, RuntimeSnapshotSource, RuntimeSummary,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION, SmpClassicPackedProfileBlock,
SmpInspectionReport, SmpLoadedSaveSlice, SmpRt3105PackedProfileBlock, SmpSaveLoadSummary,
WinInspectionReport, execute_step_command, extract_pk4_entry_file, inspect_campaign_exe_file,
inspect_pk4_file, inspect_smp_file, inspect_win_file, load_runtime_snapshot_document,
load_runtime_state_import, load_save_slice_file, project_save_slice_to_runtime_state_import,
save_runtime_save_slice_document, save_runtime_snapshot_document,
validate_runtime_snapshot_document,
};
use serde::Serialize;
use serde_json::Value;
@ -122,6 +124,10 @@ enum Command {
smp_path: PathBuf,
output_path: PathBuf,
},
RuntimeExportSaveSlice {
smp_path: PathBuf,
output_path: PathBuf,
},
RuntimeInspectPk4 {
pk4_path: PathBuf,
},
@ -237,6 +243,13 @@ struct RuntimeLoadedSaveSliceOutput {
save_slice: SmpLoadedSaveSlice,
}
#[derive(Debug, Serialize)]
struct RuntimeSaveSliceExportOutput {
path: String,
output_path: String,
save_slice_id: String,
}
#[derive(Debug, Serialize)]
struct RuntimePk4InspectionOutput {
path: String,
@ -770,6 +783,12 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
} => {
run_runtime_import_save_state(&smp_path, &output_path)?;
}
Command::RuntimeExportSaveSlice {
smp_path,
output_path,
} => {
run_runtime_export_save_slice(&smp_path, &output_path)?;
}
Command::RuntimeInspectPk4 { pk4_path } => {
run_runtime_inspect_pk4(&pk4_path)?;
}
@ -929,6 +948,14 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
output_path: PathBuf::from(output_path),
})
}
[command, subcommand, smp_path, output_path]
if command == "runtime" && subcommand == "export-save-slice" =>
{
Ok(Command::RuntimeExportSaveSlice {
smp_path: PathBuf::from(smp_path),
output_path: PathBuf::from(output_path),
})
}
[command, subcommand, path] if command == "runtime" && subcommand == "inspect-pk4" => {
Ok(Command::RuntimeInspectPk4 {
pk4_path: PathBuf::from(path),
@ -1069,7 +1096,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
})
}
_ => Err(
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
.into(),
),
}
@ -1326,6 +1353,50 @@ fn run_runtime_import_save_state(
Ok(())
}
fn run_runtime_export_save_slice(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let save_slice = load_save_slice_file(smp_path)?;
let report = export_runtime_save_slice_document(smp_path, output_path, save_slice)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
fn export_runtime_save_slice_document(
smp_path: &Path,
output_path: &Path,
save_slice: SmpLoadedSaveSlice,
) -> Result<RuntimeSaveSliceExportOutput, Box<dyn std::error::Error>> {
let document = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: smp_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("save-slice")
.to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some(format!(
"Exported loaded save slice from {}",
smp_path.display()
)),
original_save_filename: smp_path
.file_name()
.and_then(|name| name.to_str())
.map(ToString::to_string),
original_save_sha256: None,
notes: vec![],
},
save_slice,
};
save_runtime_save_slice_document(output_path, &document)?;
Ok(RuntimeSaveSliceExportOutput {
path: smp_path.display().to_string(),
output_path: output_path.display().to_string(),
save_slice_id: document.save_slice_id,
})
}
fn run_runtime_inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePk4InspectionOutput {
path: pk4_path.display().to_string(),
@ -4272,6 +4343,59 @@ mod tests {
.expect("snapshot-backed imported packed-event fixture should summarize");
}
#[test]
fn summarizes_save_slice_backed_fixtures() {
let parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-parity-save-slice-fixture.json");
let selective_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
run_runtime_summarize_fixture(&parity_fixture)
.expect("save-slice-backed parity fixture should summarize");
run_runtime_summarize_fixture(&selective_fixture)
.expect("save-slice-backed selective-import fixture should summarize");
}
#[test]
fn exports_runtime_save_slice_document_from_loaded_slice() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let output_path =
std::env::temp_dir().join(format!("rrt-export-save-slice-test-{nonce}.json"));
let smp_path = PathBuf::from("captured-test.gms");
let report = export_runtime_save_slice_document(
&smp_path,
&output_path,
SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec!["exported for test".to_string()],
},
)
.expect("save slice export should succeed");
assert_eq!(report.save_slice_id, "captured-test");
let document = rrt_runtime::load_runtime_save_slice_document(&output_path)
.expect("exported save slice document should load");
assert_eq!(document.save_slice_id, "captured-test");
assert_eq!(
document.source.original_save_filename.as_deref(),
Some("captured-test.gms")
);
let _ = fs::remove_file(output_path);
}
#[test]
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
let left = serde_json::json!({
@ -4412,6 +4536,25 @@ mod tests {
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_save_slice_backed_states_across_packed_event_boundaries() {
let left_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-parity-save-slice.json");
let right_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice.json");
let left_state = load_normalized_runtime_state(&left_path)
.expect("left save-slice-backed state should load");
let right_state = load_normalized_runtime_state(&right_path)
.expect("right save-slice-backed state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.imported_runtime_record_count"
|| entry.path == "$.packed_event_collection.records[0].decode_status"
}));
}
#[test]
fn diffs_classic_profile_samples_across_multiple_files() {
let sample_a = RuntimeClassicProfileSample {

View file

@ -1,6 +1,10 @@
use std::path::{Path, PathBuf};
use rrt_runtime::{load_runtime_snapshot_document, validate_runtime_snapshot_document};
use rrt_runtime::{
load_runtime_save_slice_document, load_runtime_snapshot_document,
project_save_slice_to_runtime_state_import, validate_runtime_save_slice_document,
validate_runtime_snapshot_document,
};
use crate::{FixtureDocument, FixtureStateOrigin, RawFixtureDocument};
@ -28,17 +32,23 @@ fn resolve_raw_fixture_document(
raw: RawFixtureDocument,
base_dir: &Path,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
let state = match (&raw.state, &raw.state_snapshot_path) {
(Some(_), Some(_)) => {
return Err(
"fixture must not specify both inline state and state_snapshot_path".into(),
);
}
(None, None) => {
return Err("fixture must specify either inline state or state_snapshot_path".into());
}
(Some(state), None) => state.clone(),
(None, Some(snapshot_path)) => {
let specified_state_inputs = usize::from(raw.state.is_some())
+ usize::from(raw.state_snapshot_path.is_some())
+ usize::from(raw.state_save_slice_path.is_some());
if specified_state_inputs != 1 {
return Err(
"fixture must specify exactly one of inline state, state_snapshot_path, or state_save_slice_path"
.into(),
);
}
let state = match (
&raw.state,
&raw.state_snapshot_path,
&raw.state_save_slice_path,
) {
(Some(state), None, None) => state.clone(),
(None, Some(snapshot_path), None) => {
let snapshot_path = resolve_snapshot_path(base_dir, snapshot_path);
let snapshot = load_runtime_snapshot_document(&snapshot_path)?;
validate_runtime_snapshot_document(&snapshot).map_err(|err| {
@ -49,11 +59,35 @@ fn resolve_raw_fixture_document(
})?;
snapshot.state
}
(None, None, Some(save_slice_path)) => {
let save_slice_path = resolve_snapshot_path(base_dir, save_slice_path);
let document = load_runtime_save_slice_document(&save_slice_path)?;
validate_runtime_save_slice_document(&document).map_err(|err| {
format!(
"invalid runtime save slice document {}: {err}",
save_slice_path.display()
)
})?;
project_save_slice_to_runtime_state_import(
&document.save_slice,
&document.save_slice_id,
document.source.description.clone(),
)
.map_err(|err| {
format!(
"failed to project runtime save slice document {}: {err}",
save_slice_path.display()
)
})?
.state
}
_ => unreachable!("state input exclusivity checked above"),
};
let state_origin = match raw.state_snapshot_path {
Some(snapshot_path) => FixtureStateOrigin::SnapshotPath(snapshot_path),
None => FixtureStateOrigin::Inline,
let state_origin = match (raw.state_snapshot_path, raw.state_save_slice_path) {
(Some(snapshot_path), None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
(None, Some(save_slice_path)) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
_ => FixtureStateOrigin::Inline,
};
Ok(FixtureDocument {
@ -82,9 +116,11 @@ mod tests {
use super::*;
use crate::FixtureStateOrigin;
use rrt_runtime::{
CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
CalendarPoint, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeSnapshotSource, RuntimeState, RuntimeWorldRestoreState,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
save_runtime_save_slice_document, save_runtime_snapshot_document,
};
use std::collections::BTreeMap;
@ -167,4 +203,76 @@ mod tests {
let _ = std::fs::remove_file(snapshot_path);
let _ = std::fs::remove_dir(fixture_dir);
}
#[test]
fn loads_fixture_from_relative_save_slice_path() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-save-slice-{nonce}"));
std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");
let save_slice_path = fixture_dir.join("state-save-slice.json");
let save_slice = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "save-slice-backed-fixture-state".to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some("test save slice".to_string()),
original_save_filename: Some("fixture.gms".to_string()),
original_save_sha256: None,
notes: vec![],
},
save_slice: rrt_runtime::SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
},
};
save_runtime_save_slice_document(&save_slice_path, &save_slice)
.expect("save slice should save");
let fixture_json = r#"
{
"format_version": 1,
"fixture_id": "save-slice-backed-fixture",
"source": {
"kind": "captured-runtime"
},
"state_save_slice_path": "state-save-slice.json",
"commands": [],
"expected_summary": {
"calendar_projection_is_placeholder": true,
"packed_event_collection_present": false
}
}
"#;
let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
.expect("save-slice-backed fixture should load");
assert_eq!(
fixture.state_origin,
FixtureStateOrigin::SaveSlicePath("state-save-slice.json".to_string())
);
assert_eq!(
fixture
.state
.metadata
.get("save_slice.import_projection")
.map(String::as_str),
Some("partial-runtime-restore-v1")
);
let _ = std::fs::remove_file(save_slice_path);
let _ = std::fs::remove_dir(fixture_dir);
}
}

View file

@ -70,6 +70,10 @@ pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub packed_event_imported_runtime_record_count: Option<usize>,
#[serde(default)]
pub packed_event_parity_only_record_count: Option<usize>,
#[serde(default)]
pub packed_event_unsupported_record_count: Option<usize>,
#[serde(default)]
pub event_runtime_record_count: Option<usize>,
#[serde(default)]
pub candidate_availability_count: Option<usize>,
@ -341,6 +345,22 @@ impl ExpectedRuntimeSummary {
));
}
}
if let Some(count) = self.packed_event_parity_only_record_count {
if actual.packed_event_parity_only_record_count != count {
mismatches.push(format!(
"packed_event_parity_only_record_count mismatch: expected {count}, got {}",
actual.packed_event_parity_only_record_count
));
}
}
if let Some(count) = self.packed_event_unsupported_record_count {
if actual.packed_event_unsupported_record_count != count {
mismatches.push(format!(
"packed_event_unsupported_record_count mismatch: expected {count}, got {}",
actual.packed_event_unsupported_record_count
));
}
}
if let Some(count) = self.event_runtime_record_count {
if actual.event_runtime_record_count != count {
mismatches.push(format!(
@ -510,6 +530,7 @@ pub struct FixtureDocument {
pub enum FixtureStateOrigin {
Inline,
SnapshotPath(String),
SaveSlicePath(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -523,6 +544,8 @@ pub struct RawFixtureDocument {
#[serde(default)]
pub state_snapshot_path: Option<String>,
#[serde(default)]
pub state_save_slice_path: Option<String>,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,

View file

@ -12,6 +12,7 @@ use crate::{
};
pub const STATE_DUMP_FORMAT_VERSION: u32 = 1;
pub const SAVE_SLICE_DOCUMENT_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeStateDumpSource {
@ -30,6 +31,27 @@ pub struct RuntimeStateDumpDocument {
pub state: RuntimeState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RuntimeSaveSliceDocumentSource {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub original_save_filename: Option<String>,
#[serde(default)]
pub original_save_sha256: Option<String>,
#[serde(default)]
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeSaveSliceDocument {
pub format_version: u32,
pub save_slice_id: String,
#[serde(default)]
pub source: RuntimeSaveSliceDocumentSource,
pub save_slice: SmpLoadedSaveSlice,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeStateImport {
pub import_id: String,
@ -581,6 +603,80 @@ pub fn validate_runtime_state_dump_document(
document.state.validate()
}
pub fn validate_runtime_save_slice_document(
document: &RuntimeSaveSliceDocument,
) -> Result<(), String> {
if document.format_version != SAVE_SLICE_DOCUMENT_FORMAT_VERSION {
return Err(format!(
"unsupported save slice document format_version {} (expected {})",
document.format_version, SAVE_SLICE_DOCUMENT_FORMAT_VERSION
));
}
if document.save_slice_id.trim().is_empty() {
return Err("save_slice_id must not be empty".to_string());
}
if document
.source
.description
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err("save slice source.description must not be empty".to_string());
}
if document
.source
.original_save_filename
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err("save slice source.original_save_filename must not be empty".to_string());
}
if document
.source
.original_save_sha256
.as_deref()
.is_some_and(|text| text.trim().is_empty())
{
return Err("save slice source.original_save_sha256 must not be empty".to_string());
}
for (index, note) in document.source.notes.iter().enumerate() {
if note.trim().is_empty() {
return Err(format!(
"save slice source.notes[{index}] must not be empty"
));
}
}
if document.save_slice.mechanism_family.trim().is_empty() {
return Err("save_slice.mechanism_family must not be empty".to_string());
}
if document.save_slice.mechanism_confidence.trim().is_empty() {
return Err("save_slice.mechanism_confidence must not be empty".to_string());
}
Ok(())
}
pub fn load_runtime_save_slice_document(
path: &Path,
) -> Result<RuntimeSaveSliceDocument, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)?;
let document: RuntimeSaveSliceDocument = serde_json::from_str(&text)?;
Ok(document)
}
pub fn save_runtime_save_slice_document(
path: &Path,
document: &RuntimeSaveSliceDocument,
) -> Result<(), Box<dyn std::error::Error>> {
validate_runtime_save_slice_document(document)
.map_err(|err| format!("invalid runtime save slice document: {err}"))?;
let bytes = serde_json::to_vec_pretty(document)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, bytes)?;
Ok(())
}
pub fn load_runtime_state_import(
path: &Path,
) -> Result<RuntimeStateImport, Box<dyn std::error::Error>> {
@ -607,6 +703,28 @@ pub fn load_runtime_state_import_from_str(
});
}
if let Ok(document) = serde_json::from_str::<RuntimeSaveSliceDocument>(text) {
validate_runtime_save_slice_document(&document)
.map_err(|err| format!("invalid runtime save slice document: {err}"))?;
let mut description_parts = Vec::new();
if let Some(description) = document.source.description {
description_parts.push(description);
}
if let Some(filename) = document.source.original_save_filename {
description_parts.push(format!("source save {filename}"));
}
let import = project_save_slice_to_runtime_state_import(
&document.save_slice,
&document.save_slice_id,
if description_parts.is_empty() {
None
} else {
Some(description_parts.join(" | "))
},
)?;
return Ok(import);
}
let state: RuntimeState = serde_json::from_str(text)?;
state
.validate()
@ -713,6 +831,84 @@ mod tests {
assert!(import.description.is_none());
}
#[test]
fn validates_and_roundtrips_save_slice_document() {
let document = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "save-slice-smoke".to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some("test save slice".to_string()),
original_save_filename: Some("smoke.gms".to_string()),
original_save_sha256: Some("deadbeef".to_string()),
notes: vec!["captured fixture".to_string()],
},
save_slice: crate::SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
},
};
assert!(validate_runtime_save_slice_document(&document).is_ok());
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("rrt-save-slice-doc-{nonce}.json"));
save_runtime_save_slice_document(&path, &document).expect("save slice doc should save");
let loaded = load_runtime_save_slice_document(&path).expect("save slice doc should load");
assert_eq!(document, loaded);
let _ = std::fs::remove_file(path);
}
#[test]
fn loads_save_slice_document_as_runtime_state_import() {
let text = serde_json::to_string(&RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "save-slice-import".to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some("test save slice import".to_string()),
original_save_filename: Some("import.gms".to_string()),
original_save_sha256: None,
notes: vec![],
},
save_slice: crate::SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
},
})
.expect("save slice doc should serialize");
let import = load_runtime_state_import_from_str(&text, "fallback")
.expect("save slice document should load as runtime import");
assert_eq!(import.import_id, "save-slice-import");
assert_eq!(
import
.state
.metadata
.get("save_slice.import_projection")
.map(String::as_str),
Some("partial-runtime-restore-v1")
);
}
#[test]
fn projects_save_slice_into_runtime_state_import() {
let save_slice = SmpLoadedSaveSlice {

View file

@ -15,9 +15,11 @@ pub use campaign_exe::{
OBSERVED_CAMPAIGN_SCENARIO_NAMES, inspect_campaign_exe_bytes, inspect_campaign_exe_file,
};
pub use import::{
RuntimeStateDumpDocument, RuntimeStateDumpSource, RuntimeStateImport,
STATE_DUMP_FORMAT_VERSION, load_runtime_state_import,
project_save_slice_to_runtime_state_import, validate_runtime_state_dump_document,
RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource, RuntimeStateDumpDocument,
RuntimeStateDumpSource, RuntimeStateImport, SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
STATE_DUMP_FORMAT_VERSION, load_runtime_save_slice_document, load_runtime_state_import,
project_save_slice_to_runtime_state_import, save_runtime_save_slice_document,
validate_runtime_save_slice_document, validate_runtime_state_dump_document,
};
pub use persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,

View file

@ -32,6 +32,8 @@ pub struct RuntimeSummary {
pub packed_event_record_count: usize,
pub packed_event_decoded_record_count: usize,
pub packed_event_imported_runtime_record_count: usize,
pub packed_event_parity_only_record_count: usize,
pub packed_event_unsupported_record_count: usize,
pub event_runtime_record_count: usize,
pub candidate_availability_count: usize,
pub zero_candidate_availability_count: usize,
@ -129,6 +131,28 @@ impl RuntimeSummary {
.as_ref()
.map(|summary| summary.imported_runtime_record_count)
.unwrap_or(0),
packed_event_parity_only_record_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| record.decode_status == "parity_only")
.count()
})
.unwrap_or(0),
packed_event_unsupported_record_count: state
.packed_event_collection
.as_ref()
.map(|summary| {
summary
.records
.iter()
.filter(|record| record.decode_status == "unsupported_framing")
.count()
})
.unwrap_or(0),
event_runtime_record_count: state.event_runtime_records.len(),
candidate_availability_count: state.candidate_availability.len(),
zero_candidate_availability_count: state

View file

@ -67,17 +67,17 @@ Current local tool status:
The atlas milestone is broad enough that the next implementation focus has already shifted downward
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
state-diff tooling, and a packed-event persistence bridge that now reaches per-record summaries and
selective executable import.
state-diff tooling, tracked save-slice documents for captured-runtime inputs, and a packed-event
persistence bridge that now reaches per-record summaries and selective executable import.
The highest-value next passes are now:
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
avoid shell-first implementation bets
- deepen captured-runtime and round-trip fixture coverage on top of the packed-event bridge that now
exists
- widen packed-event target-family coverage only where static evidence is strong enough to support
deterministic executable import
- add the next imported object/context families needed to turn current parity-only packed rows into
executable runtime records
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
environment
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice

View file

@ -22,12 +22,12 @@ Implemented today:
- snapshots, state dumps, save-slice projection, and normalized state diffing already exist in the
CLI and fixture layers
- checked-in runtime fixtures already cover deterministic stepping, periodic service, direct trigger
service, snapshot-backed inputs, normalized state-fragment assertions, and imported packed-event
execution
service, snapshot-backed inputs, save-slice-backed inputs, normalized state-fragment assertions,
and imported packed-event execution
That means the next implementation work is breadth, not bootstrap. The recommended next slice is
captured-runtime depth plus wider packed-event target-family coverage, not another persistence
scaffold pass.
wider packed-event target-family coverage plus company-collection import depth, not another
persistence scaffold pass.
## Why This Boundary
@ -220,8 +220,10 @@ Current status:
projected runtime snapshots, normalized diffs, and fixtures
- the first decoded packed-event subset can now import into executable runtime records when the
decoded actions fit the current normalized runtime-effect model
- the remaining gap is broader captured-runtime and round-trip fixture depth plus wider packed
target-family coverage, not first-pass packed-event decode
- tracked save-slice documents now provide a repo-friendly captured-runtime path without checking in
raw `.smp` binaries
- the remaining gap is wider packed target-family coverage plus company-import depth, not
first-pass captured-runtime plumbing
### Milestone 4: Domain Expansion
@ -352,6 +354,7 @@ The currently implemented normalized runtime surface is:
normalized runtime-effect vocabulary with staged event-record mutation
- save-side inspection and partial state projection for `.smp` inputs, including per-record packed
event summaries and selective executable import
- tracked save-slice documents plus save-slice-backed fixture loading for captured-runtime coverage
Checked-in fixture families already include:
@ -363,30 +366,30 @@ Checked-in fixture families already include:
## Next Slice
The recommended next implementation slice is broader captured-runtime depth on top of the packed
event bridge that now exists today.
The recommended next implementation slice is wider packed-event target-family coverage on top of the
captured save-slice workflow that now exists today.
Target behavior:
- keep the packed event bridge grounded against real captured save inputs rather than only synthetic
parser tests and snapshot fixtures
- expand the executable import subset beyond the current direct-state and follow-on lanes only when
target resolution and field semantics are statically grounded enough to preserve headless
determinism
- add the next imported object families needed to make currently parity-only packed rows executable,
starting with company-targeted effects
- keep preserving unsupported packed rows as parity summaries instead of guessing executable meaning
Public-model additions for that slice:
- additional captured-save fixture material for packed event collections
- wider target-family summaries only where imported execution can be justified by current static
evidence
- imported company/runtime context needed by the next packed-event target families
- no shell queue/modal behavior in the runtime core
Fixture work for that slice:
- captured `.smp` or save-slice-backed fixtures that prove real packed event records survive import
and diff paths
- regression fixtures that lock the current selective executable import boundary
- save-slice-backed fixtures that prove real packed event records survive import and diff paths
- regression fixtures that lock the current selective executable import boundary and the
unsupported/parity-only counts
- state-fragment assertions that lock both packed parity summaries and imported executable records
Do not mix this slice with:

View file

@ -0,0 +1,51 @@
{
"format_version": 1,
"fixture_id": "packed-event-parity-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by a tracked save-slice document with parity-heavy packed-event records."
},
"state_save_slice_path": "packed-event-parity-save-slice.json",
"commands": [
{
"kind": "step_count",
"steps": 1
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"calendar_projection_is_placeholder": true,
"packed_event_collection_present": true,
"packed_event_record_count": 2,
"packed_event_decoded_record_count": 1,
"packed_event_imported_runtime_record_count": 0,
"packed_event_parity_only_record_count": 1,
"packed_event_unsupported_record_count": 1,
"event_runtime_record_count": 0,
"total_company_cash": 0
},
"expected_state_fragment": {
"calendar": {
"tick_slot": 1
},
"metadata": {
"save_slice.import_projection": "partial-runtime-restore-v1"
},
"packed_event_collection": {
"live_entry_ids": [3, 5],
"records": [
{
"decode_status": "unsupported_framing"
},
{
"decode_status": "parity_only"
}
]
}
}
}

View file

@ -0,0 +1,123 @@
{
"format_version": 1,
"save_slice_id": "packed-event-parity-save-slice",
"source": {
"description": "Tracked save-slice document representing a parity-heavy captured packed-event collection.",
"original_save_filename": "captured-parity.gms",
"original_save_sha256": "parity-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"preserves one unsupported row and one decoded-but-parity-only row"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29696,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 5,
"live_record_count": 2,
"live_entry_ids": [3, 5],
"decoded_record_count": 1,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 3,
"payload_offset": 29186,
"payload_len": 96,
"decode_status": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": [
"real payload framing not yet decoded"
]
},
{
"record_index": 1,
"live_entry_id": 5,
"payload_offset": 29290,
"payload_len": 72,
"decode_status": "parity_only",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [
{
"label": "primary_text_band",
"packed_len": 7,
"present": true,
"preview": "Parity!"
},
{
"label": "secondary_text_band_0",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_1",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_2",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_3",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_4",
"packed_len": 0,
"present": false,
"preview": ""
}
],
"standalone_condition_row_count": 0,
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [
{
"kind": "adjust_company_cash",
"target": {
"kind": "ids",
"ids": [42]
},
"delta": 75
}
],
"executable_import_ready": false,
"notes": [
"decoded action requires explicit imported company ids before execution"
]
}
]
},
"notes": [
"parity-heavy packed-event sample"
]
}
}

View file

@ -0,0 +1,65 @@
{
"format_version": 1,
"fixture_id": "packed-event-selective-import-save-slice-fixture",
"source": {
"kind": "captured-runtime",
"description": "Fixture backed by a tracked save-slice document with one imported packed-event record and one parity-only record."
},
"state_save_slice_path": "packed-event-selective-import-save-slice.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"calendar_projection_is_placeholder": true,
"packed_event_collection_present": true,
"packed_event_record_count": 2,
"packed_event_decoded_record_count": 2,
"packed_event_imported_runtime_record_count": 1,
"packed_event_parity_only_record_count": 1,
"packed_event_unsupported_record_count": 0,
"event_runtime_record_count": 2,
"special_condition_count": 1,
"enabled_special_condition_count": 1,
"total_event_record_service_count": 2,
"total_trigger_dispatch_count": 2,
"dirty_rerun_count": 1,
"total_company_cash": 0
},
"expected_state_fragment": {
"world_flags": {
"from_packed_root": true
},
"special_conditions": {
"Imported Follow-On": 1
},
"packed_event_collection": {
"live_entry_ids": [7, 9],
"records": [
{
"decode_status": "executable",
"executable_import_ready": true
},
{
"decode_status": "parity_only",
"executable_import_ready": false
}
]
},
"event_runtime_records": [
{
"record_id": 7,
"service_count": 1
},
{
"record_id": 99,
"service_count": 1
}
],
"service_state": {
"dirty_rerun_count": 1
}
}
}

View file

@ -0,0 +1,189 @@
{
"format_version": 1,
"save_slice_id": "packed-event-selective-import-save-slice",
"source": {
"description": "Tracked save-slice document representing one executable packed-event record plus one parity-only record.",
"original_save_filename": "captured-selective-import.gms",
"original_save_sha256": "selective-import-sample-sha256",
"notes": [
"tracked as JSON save-slice document rather than raw .smp",
"locks the current selective import boundary"
]
},
"save_slice": {
"file_extension_hint": "gms",
"container_profile_family": "rt3-classic-save-container-v1",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"trailer_family": null,
"bridge_family": null,
"profile": null,
"candidate_availability_table": null,
"special_conditions_table": null,
"event_runtime_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"metadata_tag_offset": 28928,
"records_tag_offset": 29184,
"close_tag_offset": 29952,
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 9,
"live_record_count": 2,
"live_entry_ids": [7, 9],
"decoded_record_count": 2,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 7,
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": true,
"one_shot": false,
"text_bands": [
{
"label": "primary_text_band",
"packed_len": 5,
"present": true,
"preview": "Alpha"
},
{
"label": "secondary_text_band_0",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_1",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_2",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_3",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_4",
"packed_len": 0,
"present": false,
"preview": ""
}
],
"standalone_condition_row_count": 1,
"grouped_effect_row_counts": [0, 1, 0, 0],
"decoded_actions": [
{
"kind": "set_world_flag",
"key": "from_packed_root",
"value": true
},
{
"kind": "append_event_record",
"record": {
"record_id": 99,
"trigger_kind": 10,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"effects": [
{
"kind": "set_special_condition",
"label": "Imported Follow-On",
"value": 1
}
]
}
}
],
"executable_import_ready": true,
"notes": [
"fixture packed-event record"
]
},
{
"record_index": 1,
"live_entry_id": 9,
"payload_offset": 29260,
"payload_len": 72,
"decode_status": "parity_only",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [
{
"label": "primary_text_band",
"packed_len": 4,
"present": true,
"preview": "Beta"
},
{
"label": "secondary_text_band_0",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_1",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_2",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_3",
"packed_len": 0,
"present": false,
"preview": ""
},
{
"label": "secondary_text_band_4",
"packed_len": 0,
"present": false,
"preview": ""
}
],
"standalone_condition_row_count": 0,
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [
{
"kind": "adjust_company_cash",
"target": {
"kind": "ids",
"ids": [42]
},
"delta": 50
}
],
"executable_import_ready": false,
"notes": [
"decoded action still requires company import depth"
]
}
]
},
"notes": [
"mixed packed-event sample"
]
}
}