ship one record per function: merge the release set, gen reads it, descriptions as doc comments, gates for what was only claimed; v3.0
Some checks failed
CI / fuzz (push) Successful in 2m2s
CI / lint (push) Successful in 15s
CI / test (push) Failing after 18s

This commit is contained in:
Kamal Tufekcic 2026-08-02 22:01:36 +03:00
commit 3410a79b6a
28 changed files with 30596 additions and 955 deletions

View file

@ -1,5 +1,7 @@
//! source2rosetta — CLI front-end. A thin clap layer over `source2rosetta::pipeline`: parse args,
//! select the game profile, dispatch to the engine.
//! source2rosetta — CLI front-end. A thin clap layer over BOTH engine halves —
//! `source2rosetta::pipeline` (the offline derivation engine) and `source2rosetta::produce` (CI
//! orchestration plus everything that drives a running server): parse args, select the game profile,
//! dispatch.
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
@ -53,12 +55,14 @@ enum Cmd {
/// Server library to derive from; defaults to the active game's server lib.
#[arg(long)]
lib: Option<String>,
/// Seconds to wait for the server to come up and bots to spawn alive.
/// Seconds to wait for the server to come up and reach its readiness anchor — an alive bot pawn
/// for a pawn game, a live `ready_class` instance otherwise.
#[arg(long, default_value_t = 60)]
wait: u64,
#[arg(long)] // default resolved from the active game profile at dispatch
map: Option<String>,
/// Number of bots to fill the server with.
/// Number of bots to fill the server with. A pawn-less game uses this only to size `-maxplayers`;
/// nothing waits for a bot pawn there.
#[arg(long, default_value_t = 9)]
bots: u32,
/// Optional gamedata json to also validate-live against the running server.
@ -66,23 +70,26 @@ enum Cmd {
gamedata: Option<PathBuf>,
/// Write the validated (kept) gamedata here (with --gamedata) — so this one command owns the
/// server AND persists the live-validated result, no separate validate-live needed.
#[arg(long)]
#[arg(long, requires = "gamedata")]
out: Option<PathBuf>,
/// Leave the launched server running instead of killing it after the test.
#[arg(long)]
keep: bool,
/// With --gamedata, also run the LIVE fuzzer against this same server for N randomized probes
/// (0 = off). Runs against the server `produce` already launched; there is no separate command for it.
/// (0 = off). PAWN GAMES ONLY — a pawn-less game runs no live fuzz. Runs against the server THIS
/// command launched: `integration-test` boots its own and does not attach to one `produce` left
/// behind.
#[arg(long, default_value_t = 500)]
fuzz_iterations: usize,
},
/// The whole per-game build in ONE in-memory command: derive → fold → (if `--game-dir` is given)
/// validate-live + typed netvars → fold model, writing the release set (`gamedata-`/`netvars-`/`model-`/
/// `manifest`) into --out-dir. No per-stage intermediate files. **Pass `--game-dir` for a full,
/// live-validated build; omit it for a fast OFFLINE build (gamedata + model only, no server).**
/// validate-live + typed netvars → merge → fold model, writing the release set
/// (`rosetta-<game>.json` + `model-<game>.json` + `manifest.json`) into --out-dir. No per-stage
/// intermediate files. **Pass `--game-dir` for a full, live-validated build; omit it for a fast
/// OFFLINE build (no server, so no live validation and a `null` schema).**
Produce {
/// A launchable game install → the FULL build (boots a server for validate-live + typed netvars).
/// OMIT for an offline build (gamedata + model only). The offline/full switch — no separate flag.
/// OMIT for an offline build. The offline/full switch — no separate flag.
#[arg(long = "game-dir")]
game_dir: Option<PathBuf>,
/// Dir holding the on-disk libs for make-sig + live validation (defaults to --game-dir, else --target).
@ -104,7 +111,7 @@ enum Cmd {
corpus: Option<PathBuf>,
/// Corpus-signal source B: a distilled `model-<game>.json` — forward-derives from the model + only the
/// target binary (no corpus). Also triggers the sidecar fold (model N → N+1). See --corpus.
#[arg(long)]
#[arg(long, conflicts_with = "corpus")]
corpus_model: Option<PathBuf>,
/// The build DIRECTORY to DERIVE gamedata from — the primary input (its libs are searched by name).
/// A bare `.so` path is not searched; pass the directory that contains it. REQUIRED.
@ -130,9 +137,13 @@ enum Cmd {
#[arg(long)]
extra_sigs: Option<PathBuf>,
/// Declared C++ prototypes (`mappings/prototypes.json`) to judge against this build's measured
/// register footprints. Emits `abi-<game>.json`. Static repo input — omit to skip the manifest.
/// register footprints. Static repo input — omit and no function carries a declared prototype.
#[arg(long)]
prototypes: Option<PathBuf>,
/// Authored function descriptions (`mappings/semantics-<game>.json`), folded in beside each
/// function. Static repo input, keyed on the NAME — omit and no function carries one.
#[arg(long)]
semantics: Option<PathBuf>,
/// Valve's naming for the entity class behind each `PVAL_EHANDLE` Pulse parameter
/// (`mappings/ehandle-classes.json`), propagated across the parameters this build's destructor
/// addresses prove are the same type. Static repo input — omit and the bindings artifact simply
@ -228,7 +239,8 @@ enum Cmd {
out: Option<PathBuf>,
},
/// Classify how much a library changed between two builds — the CI branch primitive. Enumerates every
/// function (`.eh_frame`) in each build and compares their bodies with the position-dependent bytes
/// function in each build (relocation code-pointers decoded call targets `.eh_frame` starts —
/// the FDE list alone covers ~12% of these binaries) and compares their bodies with the position-dependent bytes
/// (RIP-relative displacements + near-branch targets) masked out, so the verdict is shift-invariant:
/// a pure layout move (bodies unchanged, addresses shifted) reads as UNCHANGED, unlike a raw byte diff.
/// Prints `skip` (nothing meaningful changed → no release), `normal` (an ordinary patch → re-derive) or
@ -249,13 +261,15 @@ enum Cmd {
lib: Option<String>,
/// Extra `skip` tolerance: a changed-fraction below this also counts as `skip`. Default 0 —
/// only a code-IDENTICAL build (0 functions changed) skips, so any real patch re-derives. Raise
/// it (e.g. 0.01) to also skip changes under N%. (Calibration on 339 CS2 pairs: 311 are
/// code-identical, real patches touch <=6 functions / <=0.08%, the 2 toolchain jumps are 34%/53%.)
/// it (e.g. 0.01) to also skip changes under N%. The default is the one setting that does not
/// depend on the calibration below: zero changed functions is zero at any denominator.
#[arg(long, default_value_t = 0.0)]
skip_below: f64,
/// changed-fraction at or above this = `shift`. Default 0.20 — the CS2 corpus's real patches top
/// out near 0.08% while its two toolchain jumps are 34%/53%, so 20% cleanly separates them with
/// wide margin and (unlike 40%) doesn't misclassify the 34% jump as an ordinary patch.
/// changed-fraction at or above this = `shift`. Default 0.20. Measured over 344 CS2 builds
/// (~70,300 functions each): 82 are code-identical, the 252 ordinary patches run from 0.001% to
/// 17.8% (median 0.12%), and the 9 toolchain jumps start at 22.4% and reach 93.8%. 0.20 sits in
/// that gap — but the gap is ~4.6 points wide, not the wide margin an earlier calibration
/// claimed, so recalibrate before trusting `shift` on another game or a re-cut corpus.
#[arg(long, default_value_t = 0.20)]
shift_above: f64,
/// Emit a machine-readable JSON object instead of the human summary.
@ -277,7 +291,8 @@ enum Cmd {
/// code-identity collapses (any real change keeps the build code-distinct).
#[arg(long, default_value_t = 0.0)]
skip_below: f64,
/// changed-fraction at or above this marks a toolchain shift = an era boundary (default 0.20).
/// changed-fraction at or above this marks a toolchain shift = an era boundary (default 0.20; see
/// `classify-change --shift-above` for what that number was measured against).
#[arg(long, default_value_t = 0.20)]
shift_above: f64,
#[arg(long)]
@ -367,6 +382,7 @@ fn main() -> Result<()> {
extra_offsets,
extra_sigs,
prototypes,
semantics,
ehandle_classes,
sig_cap,
version,
@ -406,6 +422,7 @@ fn main() -> Result<()> {
extra_offsets: inputs.extra_offsets.as_deref(),
extra_sigs: inputs.extra_sigs.as_deref(),
prototypes: prototypes.as_deref(),
semantics: semantics.as_deref(),
ehandle_classes: ehandle_classes.as_deref(),
sig_cap,
version: &version,