diff --git a/README.md b/README.md index ef955c5..4ebe436 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ The long-term direction is still a DLL we can inject into the original executabl individual functions as we build them out. The active implementation milestone is now a headless runtime rehost layer that can execute deterministic world work, compare normalized state, and grow subsystem breadth without depending on the shell or presentation path. The current packed-event -frontier is real `0x4e9a` structural decode on top of the existing save-slice, snapshot, and -overlay-import workflows. The PE32 hook remains useful as capture and integration tooling, but it -is no longer the main execution milestone. +frontier is real `0x4e9a` compact-control decode and descriptor-frontier tightening on top of the +existing save-slice, snapshot, and overlay-import workflows. The PE32 hook remains useful as +capture and integration tooling, but it is no longer the main execution milestone. ## Project Docs diff --git a/crates/rrt-fixtures/src/load.rs b/crates/rrt-fixtures/src/load.rs index 3f0ff6a..da62297 100644 --- a/crates/rrt-fixtures/src/load.rs +++ b/crates/rrt-fixtures/src/load.rs @@ -379,6 +379,7 @@ mod tests { active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), + compact_control: None, text_bands: vec![], standalone_condition_row_count: 0, standalone_condition_rows: vec![], diff --git a/crates/rrt-fixtures/src/schema.rs b/crates/rrt-fixtures/src/schema.rs index 6830278..3fbec75 100644 --- a/crates/rrt-fixtures/src/schema.rs +++ b/crates/rrt-fixtures/src/schema.rs @@ -76,6 +76,10 @@ pub struct ExpectedRuntimeSummary { #[serde(default)] pub packed_event_blocked_missing_company_context_count: Option, #[serde(default)] + pub packed_event_blocked_missing_compact_control_count: Option, + #[serde(default)] + pub packed_event_blocked_unmapped_real_descriptor_count: Option, + #[serde(default)] pub packed_event_blocked_structural_only_count: Option, #[serde(default)] pub event_runtime_record_count: Option, @@ -373,6 +377,22 @@ impl ExpectedRuntimeSummary { )); } } + if let Some(count) = self.packed_event_blocked_missing_compact_control_count { + if actual.packed_event_blocked_missing_compact_control_count != count { + mismatches.push(format!( + "packed_event_blocked_missing_compact_control_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_missing_compact_control_count + )); + } + } + if let Some(count) = self.packed_event_blocked_unmapped_real_descriptor_count { + if actual.packed_event_blocked_unmapped_real_descriptor_count != count { + mismatches.push(format!( + "packed_event_blocked_unmapped_real_descriptor_count mismatch: expected {count}, got {}", + actual.packed_event_blocked_unmapped_real_descriptor_count + )); + } + } if let Some(count) = self.packed_event_blocked_structural_only_count { if actual.packed_event_blocked_structural_only_count != count { mismatches.push(format!( diff --git a/crates/rrt-runtime/src/import.rs b/crates/rrt-runtime/src/import.rs index 7979872..12134eb 100644 --- a/crates/rrt-runtime/src/import.rs +++ b/crates/rrt-runtime/src/import.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::persistence::{load_runtime_snapshot_document, validate_runtime_snapshot_document}; use crate::{ CalendarPoint, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, + RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventCollectionSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, @@ -564,6 +565,10 @@ fn runtime_packed_event_record_summary_from_smp( active: record.active, marks_collection_dirty: record.marks_collection_dirty, one_shot: record.one_shot, + compact_control: record + .compact_control + .as_ref() + .map(runtime_packed_event_compact_control_summary_from_smp), text_bands: record .text_bands .iter() @@ -592,6 +597,23 @@ fn runtime_packed_event_record_summary_from_smp( } } +fn runtime_packed_event_compact_control_summary_from_smp( + control: &crate::SmpLoadedPackedEventCompactControlSummary, +) -> RuntimePackedEventCompactControlSummary { + RuntimePackedEventCompactControlSummary { + mode_byte_0x7ef: control.mode_byte_0x7ef, + primary_selector_0x7f0: control.primary_selector_0x7f0, + grouped_mode_0x7f4: control.grouped_mode_0x7f4, + one_shot_header_0x7f5: control.one_shot_header_0x7f5, + modifier_flag_0x7f9: control.modifier_flag_0x7f9, + modifier_flag_0x7fa: control.modifier_flag_0x7fa, + grouped_target_scope_ordinals_0x7fb: control.grouped_target_scope_ordinals_0x7fb.clone(), + grouped_scope_checkboxes_0x7ff: control.grouped_scope_checkboxes_0x7ff.clone(), + summary_toggle_0x800: control.summary_toggle_0x800, + grouped_territory_selectors_0x80f: control.grouped_territory_selectors_0x80f.clone(), + } +} + fn runtime_packed_event_text_band_summary_from_smp( band: &SmpLoadedPackedEventTextBandSummary, ) -> RuntimePackedEventTextBandSummary { @@ -803,7 +825,10 @@ fn determine_packed_event_import_outcome( return "blocked_unsupported_decode".to_string(); } if record.payload_family == "real_packed_v1" { - return "blocked_structural_only".to_string(); + if record.compact_control.is_none() { + return "blocked_missing_compact_control".to_string(); + } + return "blocked_unmapped_real_descriptor".to_string(); } if packed_record_requires_missing_company_context(record, known_company_ids) { return "blocked_missing_company_context".to_string(); @@ -1208,6 +1233,21 @@ mod tests { }] } + fn real_compact_control() -> crate::SmpLoadedPackedEventCompactControlSummary { + crate::SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: vec![0, 1, 2, 3], + grouped_scope_checkboxes_0x7ff: vec![1, 0, 1, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: vec![-1, 10, -1, 22], + } + } + #[test] fn loads_dump_document() { let text = serde_json::to_string(&RuntimeStateDumpDocument { @@ -1429,6 +1469,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), @@ -1449,6 +1490,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), @@ -1469,6 +1511,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), @@ -1679,6 +1722,7 @@ mod tests { active: Some(true), marks_collection_dirty: Some(true), one_shot: Some(false), + compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: vec![], @@ -1788,6 +1832,7 @@ mod tests { active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), + compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], @@ -1839,7 +1884,7 @@ mod tests { } #[test] - fn leaves_real_structural_records_blocked_structural_only() { + fn leaves_real_records_without_compact_control_blocked_missing_compact_control() { let save_slice = SmpLoadedSaveSlice { file_extension_hint: Some("gms".to_string()), container_profile_family: Some("rt3-classic-save-container-v1".to_string()), @@ -1876,6 +1921,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 1, standalone_condition_rows: real_condition_rows(), @@ -1903,7 +1949,7 @@ mod tests { .packed_event_collection .as_ref() .and_then(|summary| summary.records[0].import_outcome.as_deref()), - Some("blocked_structural_only") + Some("blocked_missing_compact_control") ); assert_eq!( import @@ -1923,6 +1969,85 @@ mod tests { ); } + #[test] + fn leaves_real_records_with_compact_control_blocked_unmapped_real_descriptor() { + let save_slice = 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: Some(crate::SmpLoadedEventRuntimeCollectionSummary { + source_kind: "packed-event-runtime-collection".to_string(), + mechanism_family: "classic-save-rehydrate-v1".to_string(), + mechanism_confidence: "grounded".to_string(), + container_profile_family: Some("rt3-classic-save-container-v1".to_string()), + metadata_tag_offset: 0x7100, + records_tag_offset: 0x7200, + close_tag_offset: 0x7600, + packed_state_version: 0x3e9, + packed_state_version_hex: "0x000003e9".to_string(), + live_id_bound: 7, + live_record_count: 1, + live_entry_ids: vec![7], + decoded_record_count: 1, + imported_runtime_record_count: 0, + records: vec![crate::SmpLoadedPackedEventRecordSummary { + record_index: 0, + live_entry_id: 7, + payload_offset: Some(0x7202), + payload_len: Some(133), + decode_status: "parity_only".to_string(), + payload_family: "real_packed_v1".to_string(), + trigger_kind: Some(6), + active: None, + marks_collection_dirty: None, + one_shot: Some(true), + compact_control: Some(real_compact_control()), + text_bands: packed_text_bands(), + standalone_condition_row_count: 1, + standalone_condition_rows: real_condition_rows(), + grouped_effect_row_counts: vec![1, 0, 0, 0], + grouped_effect_rows: real_grouped_rows(), + decoded_actions: vec![], + executable_import_ready: false, + notes: vec!["decoded from grounded real 0x4e9a row framing".to_string()], + }], + }), + notes: vec![], + }; + + let import = project_save_slice_to_runtime_state_import( + &save_slice, + "packed-events-real-descriptor-frontier", + None, + ) + .expect("save slice should project"); + + assert!(import.state.event_runtime_records.is_empty()); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].compact_control.as_ref()) + .map(|control| control.mode_byte_0x7ef), + Some(6) + ); + assert_eq!( + import + .state + .packed_event_collection + .as_ref() + .and_then(|summary| summary.records[0].import_outcome.as_deref()), + Some("blocked_unmapped_real_descriptor") + ); + } + #[test] fn overlays_save_slice_events_onto_base_company_context() { let base_state = RuntimeState { @@ -1997,6 +2122,7 @@ mod tests { active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), + compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], @@ -2150,6 +2276,7 @@ mod tests { active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), + compact_control: None, text_bands: packed_text_bands(), standalone_condition_row_count: 0, standalone_condition_rows: vec![], diff --git a/crates/rrt-runtime/src/lib.rs b/crates/rrt-runtime/src/lib.rs index 53e35c0..bcf0f13 100644 --- a/crates/rrt-runtime/src/lib.rs +++ b/crates/rrt-runtime/src/lib.rs @@ -37,6 +37,7 @@ pub use pk4::{ pub use runtime::{ RuntimeCompany, RuntimeCompanyTarget, RuntimeEffect, RuntimeEventRecord, RuntimeEventRecordTemplate, RuntimePackedEventCollectionSummary, + RuntimePackedEventCompactControlSummary, RuntimePackedEventConditionRowSummary, RuntimePackedEventGroupedEffectRowSummary, RuntimePackedEventRecordSummary, RuntimePackedEventTextBandSummary, RuntimeSaveProfileState, RuntimeServiceState, RuntimeState, RuntimeWorldRestoreState, @@ -47,6 +48,7 @@ pub use smp::{ SmpClassicRehydrateProfileProbe, SmpContainerProfile, SmpEarlyContentProbe, SmpHeaderVariantProbe, SmpInspectionReport, SmpKnownTagHit, SmpLoadedCandidateAvailabilityTable, SmpLoadedEventRuntimeCollectionSummary, + SmpLoadedPackedEventCompactControlSummary, SmpLoadedPackedEventConditionRowSummary, SmpLoadedPackedEventGroupedEffectRowSummary, SmpLoadedPackedEventRecordSummary, SmpLoadedPackedEventTextBandSummary, SmpLoadedProfile, SmpLoadedSaveSlice, SmpLoadedSpecialConditionsTable, SmpLocomotivePolicyFieldObservation, diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 2a09dc1..d7fe79b 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -125,6 +125,8 @@ pub struct RuntimePackedEventRecordSummary { #[serde(default)] pub one_shot: Option, #[serde(default)] + pub compact_control: Option, + #[serde(default)] pub text_bands: Vec, #[serde(default)] pub standalone_condition_row_count: usize, @@ -144,6 +146,23 @@ pub struct RuntimePackedEventRecordSummary { pub notes: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimePackedEventCompactControlSummary { + pub mode_byte_0x7ef: u8, + pub primary_selector_0x7f0: u32, + pub grouped_mode_0x7f4: u8, + pub one_shot_header_0x7f5: u32, + pub modifier_flag_0x7f9: u8, + pub modifier_flag_0x7fa: u8, + #[serde(default)] + pub grouped_target_scope_ordinals_0x7fb: Vec, + #[serde(default)] + pub grouped_scope_checkboxes_0x7ff: Vec, + pub summary_toggle_0x800: u8, + #[serde(default)] + pub grouped_territory_selectors_0x80f: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimePackedEventTextBandSummary { pub label: String, @@ -467,6 +486,23 @@ impl RuntimeState { )); } } + if let Some(control) = &record.compact_control { + if control.grouped_target_scope_ordinals_0x7fb.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].compact_control.grouped_target_scope_ordinals_0x7fb must contain exactly 4 entries" + )); + } + if control.grouped_scope_checkboxes_0x7ff.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].compact_control.grouped_scope_checkboxes_0x7ff must contain exactly 4 entries" + )); + } + if control.grouped_territory_selectors_0x80f.len() != 4 { + return Err(format!( + "packed_event_collection.records[{record_index}].compact_control.grouped_territory_selectors_0x80f must contain exactly 4 entries" + )); + } + } for row in &record.standalone_condition_rows { if row .candidate_name @@ -863,6 +899,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), @@ -884,6 +921,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 97c8c59..97e66c2 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -95,6 +95,8 @@ const PACKED_EVENT_REAL_CONDITION_MARKER: u16 = 0x526f; const PACKED_EVENT_REAL_GROUPED_EFFECT_MARKER: u16 = 0x4eb8; const PACKED_EVENT_REAL_CONDITION_ROW_LEN: usize = 0x1e; const PACKED_EVENT_REAL_GROUPED_EFFECT_ROW_LEN: usize = 0x28; +const PACKED_EVENT_REAL_GROUP_COUNT: usize = 4; +const PACKED_EVENT_REAL_COMPACT_CONTROL_LEN: usize = 37; const PACKED_EVENT_TEXT_BAND_LABELS: [&str; 6] = [ "primary_text_band", "secondary_text_band_0", @@ -1234,6 +1236,8 @@ pub struct SmpLoadedPackedEventRecordSummary { #[serde(default)] pub one_shot: Option, #[serde(default)] + pub compact_control: Option, + #[serde(default)] pub text_bands: Vec, #[serde(default)] pub standalone_condition_row_count: usize, @@ -1251,6 +1255,20 @@ pub struct SmpLoadedPackedEventRecordSummary { pub notes: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SmpLoadedPackedEventCompactControlSummary { + pub mode_byte_0x7ef: u8, + pub primary_selector_0x7f0: u32, + pub grouped_mode_0x7f4: u8, + pub one_shot_header_0x7f5: u32, + pub modifier_flag_0x7f9: u8, + pub modifier_flag_0x7fa: u8, + pub grouped_target_scope_ordinals_0x7fb: Vec, + pub grouped_scope_checkboxes_0x7ff: Vec, + pub summary_toggle_0x800: u8, + pub grouped_territory_selectors_0x80f: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SmpLoadedPackedEventTextBandSummary { pub label: String, @@ -1736,6 +1754,7 @@ fn parse_synthetic_event_runtime_record_summary( active: Some(flags & 0x01 != 0), marks_collection_dirty: Some(flags & 0x02 != 0), one_shot: Some(flags & 0x04 != 0), + compact_control: None, text_bands, standalone_condition_row_count, standalone_condition_rows: Vec::new(), @@ -1794,6 +1813,8 @@ fn parse_real_event_runtime_record_summary( }); } + let compact_control = parse_optional_real_compact_control_summary(record_body, &mut cursor)?; + if read_u16_at(record_body, cursor)? != PACKED_EVENT_REAL_CONDITION_MARKER { return None; } @@ -1851,10 +1872,13 @@ fn parse_real_event_runtime_record_summary( payload_len: Some(consumed_len), decode_status: "parity_only".to_string(), payload_family: "real_packed_v1".to_string(), - trigger_kind: None, + trigger_kind: compact_control.as_ref().map(|control| control.mode_byte_0x7ef), active: None, marks_collection_dirty: None, - one_shot: None, + one_shot: compact_control + .as_ref() + .map(|control| control.one_shot_header_0x7f5 != 0), + compact_control, text_bands, standalone_condition_row_count, standalone_condition_rows, @@ -1868,6 +1892,74 @@ fn parse_real_event_runtime_record_summary( )) } +fn parse_optional_real_compact_control_summary( + record_body: &[u8], + cursor: &mut usize, +) -> Option> { + if read_u16_at(record_body, *cursor)? == PACKED_EVENT_REAL_CONDITION_MARKER { + return Some(None); + } + + let end = cursor.checked_add(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN)?; + let bytes = record_body.get(*cursor..end)?; + let mut local = 0usize; + let mode_byte_0x7ef = read_u8_at(bytes, local)?; + local += 1; + let primary_selector_0x7f0 = read_u32_at(bytes, local)?; + local += 4; + let grouped_mode_0x7f4 = read_u8_at(bytes, local)?; + local += 1; + let one_shot_header_0x7f5 = read_u32_at(bytes, local)?; + local += 4; + let modifier_flag_0x7f9 = read_u8_at(bytes, local)?; + local += 1; + let modifier_flag_0x7fa = read_u8_at(bytes, local)?; + local += 1; + + let mut grouped_target_scope_ordinals_0x7fb = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { + grouped_target_scope_ordinals_0x7fb.push(read_u8_at(bytes, local)?); + local += 1; + } + + let mut grouped_scope_checkboxes_0x7ff = Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { + grouped_scope_checkboxes_0x7ff.push(read_u8_at(bytes, local)?); + local += 1; + } + + let summary_toggle_0x800 = read_u8_at(bytes, local)?; + local += 1; + + let mut grouped_territory_selectors_0x80f = + Vec::with_capacity(PACKED_EVENT_REAL_GROUP_COUNT); + for _ in 0..PACKED_EVENT_REAL_GROUP_COUNT { + grouped_territory_selectors_0x80f.push(read_i32_at(bytes, local)?); + local += 4; + } + + if local != bytes.len() { + return None; + } + if read_u16_at(record_body, end)? != PACKED_EVENT_REAL_CONDITION_MARKER { + return None; + } + + *cursor = end; + Some(Some(SmpLoadedPackedEventCompactControlSummary { + mode_byte_0x7ef, + primary_selector_0x7f0, + grouped_mode_0x7f4, + one_shot_header_0x7f5, + modifier_flag_0x7f9, + modifier_flag_0x7fa, + grouped_target_scope_ordinals_0x7fb, + grouped_scope_checkboxes_0x7ff, + summary_toggle_0x800, + grouped_territory_selectors_0x80f, + })) +} + fn parse_real_condition_row_summary( row_bytes: &[u8], row_index: usize, @@ -2136,13 +2228,14 @@ fn build_unsupported_event_runtime_record_summaries( payload_offset: None, payload_len: None, decode_status: "unsupported_framing".to_string(), - payload_family: "unsupported_framing".to_string(), - trigger_kind: None, - active: None, - marks_collection_dirty: None, - one_shot: None, - text_bands: Vec::new(), - standalone_condition_row_count: 0, + payload_family: "unsupported_framing".to_string(), + trigger_kind: None, + active: None, + marks_collection_dirty: None, + one_shot: None, + compact_control: None, + text_bands: Vec::new(), + standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), grouped_effect_row_counts: vec![0, 0, 0, 0], grouped_effect_rows: Vec::new(), @@ -5523,6 +5616,11 @@ fn read_u32_at(bytes: &[u8], offset: usize) -> Option { Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) } +fn read_i32_at(bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 4)?; + Some(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) +} + fn read_i64_at(bytes: &[u8], offset: usize) -> Option { let chunk = bytes.get(offset..offset + 8)?; Some(i64::from_le_bytes([ @@ -7039,8 +7137,40 @@ mod tests { bytes } + #[derive(Clone, Copy)] + struct RealCompactControlSpec { + mode_byte_0x7ef: u8, + primary_selector_0x7f0: u32, + grouped_mode_0x7f4: u8, + one_shot_header_0x7f5: u32, + modifier_flag_0x7f9: u8, + modifier_flag_0x7fa: u8, + grouped_target_scope_ordinals_0x7fb: [u8; PACKED_EVENT_REAL_GROUP_COUNT], + grouped_scope_checkboxes_0x7ff: [u8; PACKED_EVENT_REAL_GROUP_COUNT], + summary_toggle_0x800: u8, + grouped_territory_selectors_0x80f: [i32; PACKED_EVENT_REAL_GROUP_COUNT], + } + + fn build_real_compact_control(spec: RealCompactControlSpec) -> Vec { + let mut bytes = Vec::with_capacity(PACKED_EVENT_REAL_COMPACT_CONTROL_LEN); + bytes.push(spec.mode_byte_0x7ef); + bytes.extend_from_slice(&spec.primary_selector_0x7f0.to_le_bytes()); + bytes.push(spec.grouped_mode_0x7f4); + bytes.extend_from_slice(&spec.one_shot_header_0x7f5.to_le_bytes()); + bytes.push(spec.modifier_flag_0x7f9); + bytes.push(spec.modifier_flag_0x7fa); + bytes.extend_from_slice(&spec.grouped_target_scope_ordinals_0x7fb); + bytes.extend_from_slice(&spec.grouped_scope_checkboxes_0x7ff); + bytes.push(spec.summary_toggle_0x800); + for selector in spec.grouped_territory_selectors_0x80f { + bytes.extend_from_slice(&selector.to_le_bytes()); + } + bytes + } + fn build_real_event_record( text_bands: [&[u8]; 6], + compact_control: Option, condition_rows: &[Vec], grouped_rows: [&[Vec]; 4], ) -> Vec { @@ -7049,6 +7179,9 @@ mod tests { bytes.extend_from_slice(&(band.len() as u16).to_le_bytes()); bytes.extend_from_slice(band); } + if let Some(spec) = compact_control { + bytes.extend_from_slice(&build_real_compact_control(spec)); + } bytes.extend_from_slice(&PACKED_EVENT_REAL_CONDITION_MARKER.to_le_bytes()); bytes.extend_from_slice(&(condition_rows.len() as u16).to_le_bytes()); for row in condition_rows { @@ -7171,6 +7304,18 @@ mod tests { fn parses_real_style_event_runtime_record_with_zero_rows() { let record_body = build_real_event_record( [b"Alpha", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 7, + primary_selector_0x7f0: 0x63, + grouped_mode_0x7f4: 2, + one_shot_header_0x7f5: 1, + modifier_flag_0x7f9: 1, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 1, 2, 3], + grouped_scope_checkboxes_0x7ff: [1, 0, 1, 0], + summary_toggle_0x800: 1, + grouped_territory_selectors_0x80f: [-1, 10, -1, 22], + }), &[], [&[], &[], &[], &[]], ); @@ -7198,6 +7343,16 @@ mod tests { assert_eq!(summary.imported_runtime_record_count, 0); assert_eq!(summary.records[0].decode_status, "parity_only"); assert_eq!(summary.records[0].payload_family, "real_packed_v1"); + assert_eq!(summary.records[0].trigger_kind, Some(7)); + assert_eq!(summary.records[0].one_shot, Some(true)); + assert_eq!( + summary.records[0] + .compact_control + .as_ref() + .expect("real compact control should parse") + .primary_selector_0x7f0, + 0x63 + ); assert_eq!(summary.records[0].text_bands[0].preview, "Alpha"); assert_eq!(summary.records[0].standalone_condition_row_count, 0); assert_eq!(summary.records[0].standalone_condition_rows.len(), 0); @@ -7223,6 +7378,18 @@ mod tests { let group0_rows = vec![grouped_row]; let record_body = build_real_event_record( [b"Gamma", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 6, + primary_selector_0x7f0: 0x2a, + grouped_mode_0x7f4: 1, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 2, + modifier_flag_0x7fa: 3, + grouped_target_scope_ordinals_0x7fb: [1, 4, 7, 8], + grouped_scope_checkboxes_0x7ff: [0, 1, 0, 1], + summary_toggle_0x800: 0, + grouped_territory_selectors_0x80f: [11, -1, 33, -1], + }), &[condition_row], [&group0_rows, &[], &[], &[]], ); @@ -7247,6 +7414,14 @@ mod tests { .expect("event runtime collection summary should parse"); assert_eq!(summary.records[0].standalone_condition_rows.len(), 1); + assert_eq!( + summary.records[0] + .compact_control + .as_ref() + .expect("real compact control should parse") + .grouped_target_scope_ordinals_0x7fb, + vec![1, 4, 7, 8] + ); assert_eq!(summary.records[0].standalone_condition_rows[0].raw_condition_id, -1); assert_eq!( summary.records[0].standalone_condition_rows[0] @@ -7272,6 +7447,18 @@ mod tests { fn rejects_truncated_real_style_event_runtime_record() { let mut record_body = build_real_event_record( [b"Oops", b"", b"", b"", b"", b""], + Some(RealCompactControlSpec { + mode_byte_0x7ef: 5, + primary_selector_0x7f0: 0, + grouped_mode_0x7f4: 0, + one_shot_header_0x7f5: 0, + modifier_flag_0x7f9: 0, + modifier_flag_0x7fa: 0, + grouped_target_scope_ordinals_0x7fb: [0, 0, 0, 0], + grouped_scope_checkboxes_0x7ff: [0, 0, 0, 0], + summary_toggle_0x800: 0, + grouped_territory_selectors_0x80f: [0, 0, 0, 0], + }), &[], [&[], &[], &[], &[]], ); diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index e7f09a6..187acd4 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -35,6 +35,8 @@ pub struct RuntimeSummary { pub packed_event_parity_only_record_count: usize, pub packed_event_unsupported_record_count: usize, pub packed_event_blocked_missing_company_context_count: usize, + pub packed_event_blocked_missing_compact_control_count: usize, + pub packed_event_blocked_unmapped_real_descriptor_count: usize, pub packed_event_blocked_structural_only_count: usize, pub event_runtime_record_count: usize, pub candidate_availability_count: usize, @@ -169,6 +171,34 @@ impl RuntimeSummary { .count() }) .unwrap_or(0), + packed_event_blocked_missing_compact_control_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| { + record.import_outcome.as_deref() + == Some("blocked_missing_compact_control") + }) + .count() + }) + .unwrap_or(0), + packed_event_blocked_unmapped_real_descriptor_count: state + .packed_event_collection + .as_ref() + .map(|summary| { + summary + .records + .iter() + .filter(|record| { + record.import_outcome.as_deref() + == Some("blocked_unmapped_real_descriptor") + }) + .count() + }) + .unwrap_or(0), packed_event_blocked_structural_only_count: state .packed_event_collection .as_ref() @@ -271,6 +301,7 @@ mod tests { active: None, marks_collection_dirty: None, one_shot: None, + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), @@ -278,7 +309,7 @@ mod tests { grouped_effect_rows: Vec::new(), decoded_actions: Vec::new(), executable_import_ready: false, - import_outcome: Some("blocked_structural_only".to_string()), + import_outcome: Some("blocked_missing_compact_control".to_string()), notes: Vec::new(), }, RuntimePackedEventRecordSummary { @@ -292,6 +323,7 @@ mod tests { active: Some(true), marks_collection_dirty: Some(false), one_shot: Some(false), + compact_control: None, text_bands: Vec::new(), standalone_condition_row_count: 0, standalone_condition_rows: Vec::new(), @@ -311,7 +343,9 @@ mod tests { }; let summary = RuntimeSummary::from_state(&state); - assert_eq!(summary.packed_event_blocked_structural_only_count, 1); + assert_eq!(summary.packed_event_blocked_missing_compact_control_count, 1); + assert_eq!(summary.packed_event_blocked_unmapped_real_descriptor_count, 0); + assert_eq!(summary.packed_event_blocked_structural_only_count, 0); assert_eq!(summary.packed_event_blocked_missing_company_context_count, 1); } } diff --git a/docs/README.md b/docs/README.md index ec05bc8..52cf21f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -75,12 +75,14 @@ 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 -- move the packed-event parser from the synthetic harness onto real `0x4e9a` structural decode so - real rows stop collapsing to generic unsupported framing +- tighten the packed-event frontier from generic real-row structure into decoded real compact + control, so parity rows carry mode, selector, one-shot, and grouped target-scope state directly - use overlay imports as the context bridge when selectively executable packed rows still need live company state that save slices do not persist -- widen real packed-event executable coverage only after the structural decode frontier and row - summaries are stable +- widen real packed-event executable coverage only after the compact-control and descriptor frontier + is stable, not just after row framing is parsed +- keep in mind that the current local `.gms` corpus still exports with no packed event collection, + so real descriptor mapping needs to stay plumbing-first until better captures exist - 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 diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 0e2f07b..9fbd745 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -26,7 +26,8 @@ Implemented today: normalized state-fragment assertions, and imported packed-event execution That means the next implementation work is breadth, not bootstrap. The recommended next slice is -real `0x4e9a` packed-event structural decode, not another persistence scaffold pass. +real `0x4e9a` compact-control decode and descriptor-frontier tightening, not another persistence +scaffold pass. ## Why This Boundary @@ -365,33 +366,41 @@ Checked-in fixture families already include: ## Next Slice -The recommended next implementation slice is real `0x4e9a` packed-event structural decode on top -of the save-slice, snapshot, and overlay workflows that already exist today. +The recommended next implementation slice is real `0x4e9a` compact-control decode on top of the +existing real-row structural parse. Target behavior: -- parse real packed-event record bodies structurally instead of dropping them straight to - `unsupported_framing` -- preserve the first real decode pass as parity-only, exposing text-band and row-family structure - without guessing executable semantics +- decode the compact control block that sits above the real standalone-condition and grouped-effect + row families, carrying through raw grounded lanes such as mode byte `0x7ef`, primary selector + `0x7f0`, grouped mode `0x7f4`, one-shot header `0x7f5`, modifier bytes `0x7f9/0x7fa`, grouped + target-scope ordinals `0x7fb`, grouped scope checkboxes `0x7ff`, summary toggle `0x800`, and + grouped territory selectors `0x80f` +- keep real rows parity-only in runtime import, but replace the coarse `blocked_structural_only` + frontier with narrower outcomes such as `blocked_missing_compact_control` and + `blocked_unmapped_real_descriptor` - keep the existing synthetic harness and overlay-backed executable import path working unchanged -- reserve selective executable import from real rows for a later pass once row semantics are tighter +- reserve the first real descriptor-to-effect mapping for a later slice once captured evidence is + tighter Public-model additions for that slice: -- payload-family labeling that distinguishes synthetic harness records from real packed rows and - unsupported framing -- structural row summaries for real standalone condition rows and grouped effect rows -- runtime-side import outcome labels that distinguish `blocked_structural_only` from - `blocked_missing_company_context` and `blocked_unsupported_decode` +- compact-control summaries on packed-event records in both the save-side and runtime-side models +- runtime summary counts for compact-control-missing and unmapped-real-descriptor blockers +- trigger-kind and one-shot derivation only where the compact-control mapping is already grounded Fixture work for that slice: -- one parity-heavy tracked sample that now exposes a real structurally decoded row family +- update the parity-heavy tracked sample so the real row includes compact-control state - regression fixtures that keep synthetic executable import and overlay-backed company-context upgrade behavior green -- state-fragment assertions that lock the new structural row summaries and - `blocked_structural_only` frontier +- state-fragment assertions that lock the new compact-control summary and narrower import blockers + +Current local constraint: + +- the local checked-in and on-disk `.gms` corpus currently exports with + `packed_event_collection_present = false`, so this slice must not depend on a newly captured real + packed-event-bearing save for acceptance Do not mix this slice with: diff --git a/fixtures/runtime/packed-event-parity-save-slice-fixture.json b/fixtures/runtime/packed-event-parity-save-slice-fixture.json index e6fe58b..7501a79 100644 --- a/fixtures/runtime/packed-event-parity-save-slice-fixture.json +++ b/fixtures/runtime/packed-event-parity-save-slice-fixture.json @@ -26,7 +26,9 @@ "packed_event_imported_runtime_record_count": 0, "packed_event_parity_only_record_count": 1, "packed_event_unsupported_record_count": 1, - "packed_event_blocked_structural_only_count": 1, + "packed_event_blocked_missing_compact_control_count": 0, + "packed_event_blocked_unmapped_real_descriptor_count": 1, + "packed_event_blocked_structural_only_count": 0, "event_runtime_record_count": 0, "total_company_cash": 0 }, @@ -47,7 +49,13 @@ { "decode_status": "parity_only", "payload_family": "real_packed_v1", - "import_outcome": "blocked_structural_only", + "trigger_kind": 6, + "one_shot": true, + "import_outcome": "blocked_unmapped_real_descriptor", + "compact_control": { + "primary_selector_0x7f0": 99, + "grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3] + }, "standalone_condition_rows": [ { "candidate_name": "AutoPlant" diff --git a/fixtures/runtime/packed-event-parity-save-slice.json b/fixtures/runtime/packed-event-parity-save-slice.json index d14a243..a85a557 100644 --- a/fixtures/runtime/packed-event-parity-save-slice.json +++ b/fixtures/runtime/packed-event-parity-save-slice.json @@ -54,9 +54,23 @@ "record_index": 1, "live_entry_id": 5, "payload_offset": 29290, - "payload_len": 72, + "payload_len": 109, "decode_status": "parity_only", "payload_family": "real_packed_v1", + "trigger_kind": 6, + "one_shot": true, + "compact_control": { + "mode_byte_0x7ef": 6, + "primary_selector_0x7f0": 99, + "grouped_mode_0x7f4": 2, + "one_shot_header_0x7f5": 1, + "modifier_flag_0x7f9": 1, + "modifier_flag_0x7fa": 0, + "grouped_target_scope_ordinals_0x7fb": [0, 1, 2, 3], + "grouped_scope_checkboxes_0x7ff": [1, 0, 1, 0], + "summary_toggle_0x800": 1, + "grouped_territory_selectors_0x80f": [-1, 10, -1, 22] + }, "text_bands": [ { "label": "primary_text_band", @@ -137,7 +151,7 @@ "decoded_actions": [], "executable_import_ready": false, "notes": [ - "decoded from grounded real 0x4e9a row framing" + "decoded from grounded real 0x4e9a row framing with compact control" ] } ]