diff --git a/src/produce.rs b/src/produce.rs index f375977..84b486c 100644 --- a/src/produce.rs +++ b/src/produce.rs @@ -2124,17 +2124,30 @@ fn validate_live_cmd( fn verify_live_cmd(prof: &GameProfile, pid: u32, dir: &Path, lib: &str) -> Result { let img = load_lib(dir, lib)?; let classes = schema::enumerate_schema(&img); - // The SAME floor `produce` applies, not merely non-empty: this half compares an offline read against a - // live one through the same `CI_*` constants on the same bytes, so a reshape's survivors agree with - // themselves at ~1.0 and a handful of classes looks like a clean run. See - // `GameProfile::min_schema_classes`. - ensure!( - classes.len() >= prof.min_schema_classes, - "offline schema derivation found {} classes in {lib} (floor {}) — refusing to verify a schema \ - whose class table collapsed", - classes.len(), - prof.min_schema_classes - ); + // A floor, not merely non-empty: this half compares an offline read against a live one through the same + // `CI_*` constants on the same bytes, so a reshape's survivors agree with themselves at ~1.0 and a + // handful of classes looks like a clean run. + // + // It is `min_schema_classes_LIB`, because this enumerates ONE library while `produce`'s floor counts the + // union across all of them — see `GameProfile::min_schema_classes_lib`. Only `server_lib` has a + // calibrated count, so any other library is enumerated and reported rather than judged against a number + // that does not describe it. + if lib == prof.server_lib { + ensure!( + classes.len() >= prof.min_schema_classes_lib, + "offline schema derivation found {} classes in {lib} (floor {}) — refusing to verify a schema \ + whose class table collapsed", + classes.len(), + prof.min_schema_classes_lib + ); + } else { + eprintln!( + "NOTE: {lib} is not {}, which is the only library with a calibrated class floor — \ + enumerated {} classes, collapse check SKIPPED", + prof.server_lib, + classes.len() + ); + } let live = live::LiveProcess::attach(pid)?; let base = live diff --git a/src/profile.rs b/src/profile.rs index b5091d8..360ac48 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -154,6 +154,16 @@ pub struct GameProfile { /// offline/live layout comparison reads the same bytes through the same `CI_*` constants, so whatever /// survives a reshape agrees with itself. pub min_schema_classes: usize, + /// Collapse floor for the schema CLASS table read from a SINGLE library — the live oracle's population. + /// + /// Distinct from [`min_schema_classes`](Self::min_schema_classes), and the two may never be shared: that + /// one counts the union across every mapped library, this one counts `server_lib` alone, and the union is + /// roughly twice as large. A floor calibrated on the union rejects every healthy build when applied here, + /// because the honest single-library count sits below it by construction. + /// + /// Calibrated the same way as its sibling — well under the observed count, a collapse detector rather + /// than a tight bound — and it only applies to `server_lib`, the one library whose count is calibrated. + pub min_schema_classes_lib: usize, /// Collapse floor for the DERIVED function tiers — `core + high_confidence`. /// /// Every table read out of the binary has one of these; the tool's headline product did not, and the @@ -252,9 +262,11 @@ pub const CS2: GameProfile = GameProfile { // observed live: 271 of 300 bindings attributed across 24 classes min_vscript_classed: 150, min_schema_enums: 250, - // CS2 recovers 1,899. A floor at 1,200 is well clear of build-to-build drift and nowhere near - // the range a `SchemaClassInfoData_t` reshape would leave. + // CS2 recovers 1,899 across every mapped library. A floor at 1,200 is well clear of build-to-build + // drift and nowhere near the range a `SchemaClassInfoData_t` reshape would leave. min_schema_classes: 1_200, + // libserver.so alone holds 852 of those; the live oracle reads that library only. + min_schema_classes_lib: 550, // CS2 ships 1,086 core + 2,899 high-confidence = 3,985. min_core_functions: 2_500, game_key: "csgo", @@ -365,8 +377,10 @@ pub const DOTA: GameProfile = GameProfile { // observed live: 1,638 of 1,841 bindings attributed across 63 classes min_vscript_classed: 900, min_schema_enums: 350, - // Dota recovers 2,962. + // Dota recovers 2,962 across every mapped library. min_schema_classes: 2_000, + // libserver.so alone holds 1,916 of those; the live oracle reads that library only. + min_schema_classes_lib: 1_250, // Dota ships 1,096 + 4,047 = 5,143. min_core_functions: 3_000, game_key: "dota", @@ -450,6 +464,23 @@ pub const DOTA: GameProfile = GameProfile { mod tests { use super::*; + /// The two class floors count DIFFERENT populations — the all-library union and `server_lib` alone — + /// so a profile that gives them the same value has calibrated one of them against the other's + /// population, which rejects every healthy build on whichever site got the larger number. + #[test] + fn the_single_library_class_floor_is_strictly_below_the_all_library_one() { + for prof in [&CS2, &DOTA] { + assert!( + prof.min_schema_classes_lib < prof.min_schema_classes, + "{}: single-library floor {} must sit below the all-library floor {} — one library \ + cannot hold more classes than every library", + prof.token, + prof.min_schema_classes_lib, + prof.min_schema_classes + ); + } + } + #[test] fn cs2_launch_args_are_byte_identical_to_the_old_hand_synced_vec() { // The exact arg vec the live launch requires for map="de_dust2", bots=9 — pins the LaunchSpec