diff --git a/crates/rrt-cli/src/main.rs b/crates/rrt-cli/src/main.rs index bac1f98..c23b282 100644 --- a/crates/rrt-cli/src/main.rs +++ b/crates/rrt-cli/src/main.rs @@ -246,6 +246,9 @@ enum Command { RuntimeScanCandidateTableHeaders { root_path: PathBuf, }, + RuntimeScanCandidateTableNamedRuns { + root_path: PathBuf, + }, RuntimeScanSpecialConditions { root_path: PathBuf, }, @@ -857,6 +860,41 @@ struct RuntimeCandidateTableHeaderScanSample { zero_trailer_entry_names: Vec, } +#[derive(Debug, Clone, Serialize)] +struct RuntimeCandidateTableNamedRun { + prefix: String, + start_index: usize, + end_index: usize, + count: usize, + first_name: String, + last_name: String, + start_offset: usize, + end_offset: usize, + distinct_trailer_hex_words: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct RuntimeCandidateTableNamedRunScanSample { + path: String, + profile_family: String, + source_kind: String, + observed_entry_count: usize, + port_runs: Vec, + warehouse_runs: Vec, +} + +#[derive(Debug, Serialize)] +struct RuntimeCandidateTableNamedRunScanReport { + root_path: String, + file_count: usize, + files_with_probe_count: usize, + files_with_any_numbered_port_runs_count: usize, + files_with_any_numbered_warehouse_runs_count: usize, + files_with_both_numbered_run_families_count: usize, + skipped_file_count: usize, + samples: Vec, +} + #[derive(Debug, Clone, Serialize)] struct RuntimeSpecialConditionsScanSample { path: String, @@ -1220,6 +1258,9 @@ fn real_main() -> Result<(), Box> { Command::RuntimeScanCandidateTableHeaders { root_path } => { run_runtime_scan_candidate_table_headers(&root_path)?; } + Command::RuntimeScanCandidateTableNamedRuns { root_path } => { + run_runtime_scan_candidate_table_named_runs(&root_path)?; + } Command::RuntimeScanSpecialConditions { root_path } => { run_runtime_scan_special_conditions(&root_path)?; } @@ -1603,6 +1644,13 @@ fn parse_command() -> Result> { root_path: PathBuf::from(root_path), }) } + [command, subcommand, root_path] + if command == "runtime" && subcommand == "scan-candidate-table-named-runs" => + { + Ok(Command::RuntimeScanCandidateTableNamedRuns { + root_path: PathBuf::from(root_path), + }) + } [command, subcommand, root_path] if command == "runtime" && subcommand == "scan-special-conditions" => { @@ -1647,7 +1695,7 @@ fn parse_command() -> Result> { }) } _ => Err( - "usage: rrt-cli [validate [repo-root] | finance eval | finance diff | runtime validate-fixture | runtime summarize-fixture | runtime export-fixture-state | runtime diff-state | runtime summarize-state | runtime import-state | runtime inspect-smp | runtime inspect-candidate-table | runtime inspect-compact-event-dispatch-cluster | runtime inspect-compact-event-dispatch-cluster-counts | runtime inspect-map-title-hints | runtime summarize-save-load | runtime load-save-slice | runtime inspect-save-company-chairman | runtime inspect-save-placed-structure-triplets | runtime compare-region-fixed-row-runs | runtime inspect-periodic-company-service-trace | runtime inspect-region-service-trace | runtime inspect-infrastructure-asset-trace | runtime inspect-save-region-queued-notice-records | runtime inspect-placed-structure-dynamic-side-buffer | runtime inspect-unclassified-save-collections | runtime import-save-state | runtime export-save-slice | runtime export-overlay-import | runtime inspect-pk4 | runtime inspect-cargo-types | runtime inspect-building-type-sources [building-bindings.json] | runtime inspect-cargo-skins | runtime inspect-cargo-economy-sources | runtime inspect-cargo-production-selector | runtime inspect-cargo-price-selector | runtime inspect-win | runtime extract-pk4-entry | runtime inspect-campaign-exe | runtime compare-classic-profile [saveN.gms...] | runtime compare-105-profile [saveN.gms...] | runtime compare-candidate-table [fileN...] | runtime compare-recipe-book-lines [fileN...] | runtime compare-setup-payload-core [fileN...] | runtime compare-setup-launch-payload [fileN...] | runtime compare-post-special-conditions-scalars [fileN...] | runtime scan-candidate-table-headers | runtime scan-special-conditions | runtime scan-aligned-runtime-rule-band | runtime scan-post-special-conditions-scalars | runtime scan-post-special-conditions-tail | runtime scan-recipe-book-lines | runtime export-profile-block ]" + "usage: rrt-cli [validate [repo-root] | finance eval | finance diff | runtime validate-fixture | runtime summarize-fixture | runtime export-fixture-state | runtime diff-state | runtime summarize-state | runtime import-state | runtime inspect-smp | runtime inspect-candidate-table | runtime inspect-compact-event-dispatch-cluster | runtime inspect-compact-event-dispatch-cluster-counts | runtime inspect-map-title-hints | runtime summarize-save-load | runtime load-save-slice | runtime inspect-save-company-chairman | runtime inspect-save-placed-structure-triplets | runtime compare-region-fixed-row-runs | runtime inspect-periodic-company-service-trace | runtime inspect-region-service-trace | runtime inspect-infrastructure-asset-trace | runtime inspect-save-region-queued-notice-records | runtime inspect-placed-structure-dynamic-side-buffer | runtime inspect-unclassified-save-collections | runtime import-save-state | runtime export-save-slice | runtime export-overlay-import | runtime inspect-pk4 | runtime inspect-cargo-types | runtime inspect-building-type-sources [building-bindings.json] | runtime inspect-cargo-skins | runtime inspect-cargo-economy-sources | runtime inspect-cargo-production-selector | runtime inspect-cargo-price-selector | runtime inspect-win | runtime extract-pk4-entry | runtime inspect-campaign-exe | runtime compare-classic-profile [saveN.gms...] | runtime compare-105-profile [saveN.gms...] | runtime compare-candidate-table [fileN...] | runtime compare-recipe-book-lines [fileN...] | runtime compare-setup-payload-core [fileN...] | runtime compare-setup-launch-payload [fileN...] | runtime compare-post-special-conditions-scalars [fileN...] | runtime scan-candidate-table-headers | runtime scan-candidate-table-named-runs | runtime scan-special-conditions | runtime scan-aligned-runtime-rule-band | runtime scan-post-special-conditions-scalars | runtime scan-post-special-conditions-tail | runtime scan-recipe-book-lines | runtime export-profile-block ]" .into(), ), } @@ -2938,6 +2986,50 @@ fn run_runtime_scan_candidate_table_headers( Ok(()) } +fn run_runtime_scan_candidate_table_named_runs( + root_path: &Path, +) -> Result<(), Box> { + let mut candidate_paths = Vec::new(); + collect_candidate_table_input_paths(root_path, &mut candidate_paths)?; + + let file_count = candidate_paths.len(); + let mut samples = Vec::new(); + let mut skipped_file_count = 0usize; + for path in candidate_paths { + match load_candidate_table_named_run_scan_sample(&path) { + Ok(sample) => samples.push(sample), + Err(_) => skipped_file_count += 1, + } + } + + let files_with_probe_count = samples.len(); + let files_with_any_numbered_port_runs_count = samples + .iter() + .filter(|sample| !sample.port_runs.is_empty()) + .count(); + let files_with_any_numbered_warehouse_runs_count = samples + .iter() + .filter(|sample| !sample.warehouse_runs.is_empty()) + .count(); + let files_with_both_numbered_run_families_count = samples + .iter() + .filter(|sample| !sample.port_runs.is_empty() && !sample.warehouse_runs.is_empty()) + .count(); + + let report = RuntimeCandidateTableNamedRunScanReport { + root_path: root_path.display().to_string(), + file_count, + files_with_probe_count, + files_with_any_numbered_port_runs_count, + files_with_any_numbered_warehouse_runs_count, + files_with_both_numbered_run_families_count, + skipped_file_count, + samples, + }; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} + fn run_runtime_scan_special_conditions(root_path: &Path) -> Result<(), Box> { let mut candidate_paths = Vec::new(); collect_special_conditions_input_paths(root_path, &mut candidate_paths)?; @@ -4152,6 +4244,23 @@ fn load_candidate_table_header_scan_sample( }) } +fn load_candidate_table_named_run_scan_sample( + smp_path: &Path, +) -> Result> { + let report = load_candidate_table_inspection_report(smp_path)?; + let port_runs = collect_numbered_candidate_name_runs(&report.entries, "Port"); + let warehouse_runs = collect_numbered_candidate_name_runs(&report.entries, "Warehouse"); + + Ok(RuntimeCandidateTableNamedRunScanSample { + path: report.path, + profile_family: report.profile_family, + source_kind: report.source_kind, + observed_entry_count: report.observed_entry_count, + port_runs, + warehouse_runs, + }) +} + fn load_special_conditions_scan_sample( smp_path: &Path, ) -> Result> { @@ -4495,6 +4604,61 @@ fn collect_candidate_table_input_paths( Ok(()) } +fn collect_numbered_candidate_name_runs( + entries: &[RuntimeCandidateTableEntrySample], + prefix: &str, +) -> Vec { + let mut numbered_entries = entries + .iter() + .filter_map(|entry| { + parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number)) + }) + .collect::>(); + numbered_entries.sort_by_key(|(entry, number)| (entry.index, *number)); + + let mut runs = Vec::new(); + let mut cursor = 0usize; + while cursor < numbered_entries.len() { + let (first_entry, first_number) = numbered_entries[cursor]; + let mut last_entry = first_entry; + let mut last_number = first_number; + let mut distinct_trailer_hex_words = BTreeSet::from([first_entry.trailer_word_hex.clone()]); + let mut next = cursor + 1; + while next < numbered_entries.len() { + let (entry, number) = numbered_entries[next]; + if entry.index != last_entry.index + 1 || number != last_number + 1 { + break; + } + distinct_trailer_hex_words.insert(entry.trailer_word_hex.clone()); + last_entry = entry; + last_number = number; + next += 1; + } + runs.push(RuntimeCandidateTableNamedRun { + prefix: prefix.to_string(), + start_index: first_entry.index, + end_index: last_entry.index, + count: next - cursor, + first_name: first_entry.text.clone(), + last_name: last_entry.text.clone(), + start_offset: first_entry.offset, + end_offset: last_entry.offset, + distinct_trailer_hex_words: distinct_trailer_hex_words.into_iter().collect(), + }); + cursor = next; + } + + runs +} + +fn parse_numbered_candidate_name(text: &str, prefix: &str) -> Option { + let digits = text.strip_prefix(prefix)?; + if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + digits.parse().ok() +} + fn collect_special_conditions_input_paths( root_path: &Path, out: &mut Vec, @@ -6551,6 +6715,83 @@ mod tests { ); } + #[test] + fn collects_numbered_candidate_name_runs_by_prefix() { + let entries = vec![ + RuntimeCandidateTableEntrySample { + index: 35, + offset: 28535, + text: "Port00".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + RuntimeCandidateTableEntrySample { + index: 43, + offset: 28807, + text: "Warehouse00".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + RuntimeCandidateTableEntrySample { + index: 45, + offset: 28875, + text: "Port01".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + RuntimeCandidateTableEntrySample { + index: 46, + offset: 28909, + text: "Port02".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + RuntimeCandidateTableEntrySample { + index: 56, + offset: 29249, + text: "Warehouse01".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + RuntimeCandidateTableEntrySample { + index: 57, + offset: 29283, + text: "Warehouse02".to_string(), + availability_dword: 1, + availability_dword_hex: "0x00000001".to_string(), + trailer_word: 1, + trailer_word_hex: "0x00000001".to_string(), + }, + ]; + + let port_runs = collect_numbered_candidate_name_runs(&entries, "Port"); + let warehouse_runs = collect_numbered_candidate_name_runs(&entries, "Warehouse"); + + assert_eq!(port_runs.len(), 2); + assert_eq!(port_runs[0].first_name, "Port00"); + assert_eq!(port_runs[0].count, 1); + assert_eq!(port_runs[1].first_name, "Port01"); + assert_eq!(port_runs[1].last_name, "Port02"); + assert_eq!(port_runs[1].count, 2); + + assert_eq!(warehouse_runs.len(), 2); + assert_eq!(warehouse_runs[0].first_name, "Warehouse00"); + assert_eq!(warehouse_runs[0].count, 1); + assert_eq!(warehouse_runs[1].first_name, "Warehouse01"); + assert_eq!(warehouse_runs[1].last_name, "Warehouse02"); + assert_eq!(warehouse_runs[1].count, 2); + } + #[test] fn diffs_recipe_book_line_samples_across_multiple_files() { let sample_a = RuntimeRecipeBookLineSample { diff --git a/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md b/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md index ca06fa8..23d74f5 100644 --- a/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md +++ b/docs/control-loop-atlas/runtime-roots-camera-and-support-families.md @@ -1244,6 +1244,17 @@ problem is no longer whether those names exist as stable scenario rows; it is how that fixed candidate-table cluster is projected into the later aux-record bank and then into the live clone families. + The root scan narrows the real corpus for that question too. `runtime scan-candidate-table-headers + rt3_wineprefix/drive_c/rt3/maps` shows `37` probe-bearing shipped maps and `4` skips, while the + narrower `runtime scan-candidate-table-named-runs` command confirms that `Louisiana.gmp` and + `Dutchlantis.gmp` share the same split shape: + isolated `Port00` at row `35`, contiguous `Port01..11` at rows `45..55`, isolated + `Warehouse00` at row `43`, and contiguous `Warehouse01..11` at rows `56..66`. Raw string + presence is broader than that actual fixed-candidate-table seam too: `Port00` appears in all + `41` shipped `.gmp` files, but `Central Pacific.gmp`, `Italy.gmp`, `Tex-Mex.gmp`, and + `Texas Tea.gmp` do not expose the fixed candidate-table header at all. So the next Tier-2 + source pass should target the `37` probe-bearing maps rather than the noisier full + string-bearing map corpus. The direct `+0xba/+0xbb` writer census now rules out a broad false lead too. The obvious new stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string refresh helpers over a different id-keyed collection rooted through diff --git a/docs/rehost-queue.md b/docs/rehost-queue.md index 4abe384..7d7ddc7 100644 --- a/docs/rehost-queue.md +++ b/docs/rehost-queue.md @@ -755,6 +755,17 @@ Working rule: is no longer whether those names exist as stable scenario rows; it is how that stable candidate-table cluster is projected into the later aux-record bank and then into the live clone families. + The new root scan sharpens that boundary further. `runtime scan-candidate-table-headers + rt3_wineprefix/drive_c/rt3/maps` shows `37` probe-bearing shipped maps and `4` skips, while + the narrower `runtime scan-candidate-table-named-runs` command confirms that + `Louisiana.gmp` and `Dutchlantis.gmp` share the same split shape: + `Port00` as an isolated run at row `35`, `Port01..11` as one contiguous run at rows `45..55`, + `Warehouse00` isolated at row `43`, and `Warehouse01..11` contiguous at rows `56..66`. + Raw map-string presence is broader than that actual candidate-table seam too: + `Port00` appears in all `41` shipped `.gmp` files, but `Central Pacific.gmp`, `Italy.gmp`, + `Tex-Mex.gmp`, and `Texas Tea.gmp` do not expose the fixed candidate-table header at all. So + the next Tier-2 source pass should target the `37` probe-bearing maps rather than the noisier + full string-bearing map corpus. The direct `+0xba/+0xbb` writer census is narrower now too. The obvious newly surfaced stores at `0x004ecd42/0x004ecdaa` and `0x004ed5d5/0x004ed625` are only shell-side portrait/string refresh helpers: they walk a separate id-keyed collection through `0x0053f830`, free and