gameplay: compose recovered exit rules
Browse files- crates/urt-gameplay/src/bin/urt-lifecycle.rs +71 -0
- crates/urt-gameplay/src/exit_rules.rs +154 -0
- docs/FINDINGS.md +3 -0
- docs/PROVISIONAL_TODO.md +6 -0
- docs/RESEARCH_LEDGER.md +9 -0
- docs/REVERSE_ENGINEERING_TODO.md +6 -0
- docs/TESTING.md +5 -0
- docs/research/QVM_FLOAT_EXIT_RULES_BATCH.md +37 -0
- docs/research/README.md +5 -0
- docs/research/RUST_EXIT_COMPLETE.md +7 -0
- docs/research/rust-exit-complete.json +0 -0
- docs/research/rust-health-impulse.json +1 -1
- scripts/analyze_rust_exit_complete.py +31 -0
- scripts/test_rust_exit_complete.py +13 -0
- xtask/checks.tsv +18 -0
- xtask/src/lib.rs +5 -0
crates/urt-gameplay/src/bin/urt-lifecycle.rs
CHANGED
|
@@ -426,6 +426,77 @@ fn run() -> Result<(), String> {
|
|
| 426 |
second.cached_ping as u32
|
| 427 |
);
|
| 428 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
"exit-timer" if w.len() >= 12 && w[10] <= 64 && w[11] <= 64 => {
|
| 430 |
use urt_gameplay::exit_rules::{TimerHit, TimerState, apply_timer};
|
| 431 |
let n = w[10] as usize;
|
|
|
|
| 426 |
second.cached_ping as u32
|
| 427 |
);
|
| 428 |
}
|
| 429 |
+
"exit-complete" if w.len() >= 22 && w[20] <= 64 && w[21] <= 64 => {
|
| 430 |
+
use urt_gameplay::exit_rules::{
|
| 431 |
+
Decision, LimitHit, LimitState, State, TimerState, advance,
|
| 432 |
+
};
|
| 433 |
+
let n = w[20] as usize;
|
| 434 |
+
let e = w[21] as usize;
|
| 435 |
+
if w.len() != 22 + 10 * n + e {
|
| 436 |
+
return Err("invalid complete exit clients".into());
|
| 437 |
+
}
|
| 438 |
+
let mut clients: Vec<_> = w[22..22 + 10 * n]
|
| 439 |
+
.chunks_exact(10)
|
| 440 |
+
.map(exit_client)
|
| 441 |
+
.collect();
|
| 442 |
+
let mut s = State {
|
| 443 |
+
blocked: w[0] != 0,
|
| 444 |
+
pending: w[1] as i32,
|
| 445 |
+
slow: w[2] != 0,
|
| 446 |
+
predicate: w[19] != 0,
|
| 447 |
+
timer: TimerState {
|
| 448 |
+
now: w[3] as i32,
|
| 449 |
+
mode: w[4] as i32,
|
| 450 |
+
client_count: w[6] as usize,
|
| 451 |
+
round_limit: w[7] as i32,
|
| 452 |
+
round_active: w[8] != 0,
|
| 453 |
+
round_count: w[9] as i32,
|
| 454 |
+
countdown: w[10] as i32,
|
| 455 |
+
time_limit: w[11] as i32,
|
| 456 |
+
paused: w[12] != 0,
|
| 457 |
+
start: w[13] as i32,
|
| 458 |
+
},
|
| 459 |
+
limits: LimitState {
|
| 460 |
+
mode: w[4] as i32,
|
| 461 |
+
max_clients: w[5] as usize,
|
| 462 |
+
playing: w[14] as i32,
|
| 463 |
+
score_limit: w[15] as i32,
|
| 464 |
+
team_limit: w[16] as i32,
|
| 465 |
+
scores: [w[17] as i32, w[18] as i32],
|
| 466 |
+
},
|
| 467 |
+
};
|
| 468 |
+
let p = advance(&mut s, &mut clients, &w[22 + 10 * n..])
|
| 469 |
+
.map_err(|e| format!("unsupported input: {e:?}"))?;
|
| 470 |
+
let (kind, id) = match p.decision {
|
| 471 |
+
Decision::Blocked => ("blocked", -1),
|
| 472 |
+
Decision::Wait => ("wait", -1),
|
| 473 |
+
Decision::Transition => ("transition", -1),
|
| 474 |
+
Decision::Class(id) => ("class", id as i32),
|
| 475 |
+
Decision::Predicate => ("predicate", -1),
|
| 476 |
+
Decision::Round => ("round", -1),
|
| 477 |
+
Decision::Time => ("time", -1),
|
| 478 |
+
Decision::None => ("none", -1),
|
| 479 |
+
Decision::Limit(hit) => match hit {
|
| 480 |
+
LimitHit::LowFirst => ("low_first", -1),
|
| 481 |
+
LimitHit::LowSecond => ("low_second", -1),
|
| 482 |
+
LimitHit::HighFirst => ("high_first", -1),
|
| 483 |
+
LimitHit::HighSecond => ("high_second", -1),
|
| 484 |
+
LimitHit::Client(id) => ("client", id as i32),
|
| 485 |
+
},
|
| 486 |
+
};
|
| 487 |
+
println!(
|
| 488 |
+
"{{\"kind\":\"{}\",\"client\":{},\"pending\":{},\"clients\":[{}],\"resets\":[{}]}}",
|
| 489 |
+
kind,
|
| 490 |
+
id,
|
| 491 |
+
s.pending as u32,
|
| 492 |
+
clients
|
| 493 |
+
.iter()
|
| 494 |
+
.map(exit_client_json)
|
| 495 |
+
.collect::<Vec<_>>()
|
| 496 |
+
.join(","),
|
| 497 |
+
exit_resets_json(&p.resets)
|
| 498 |
+
);
|
| 499 |
+
}
|
| 500 |
"exit-timer" if w.len() >= 12 && w[10] <= 64 && w[11] <= 64 => {
|
| 501 |
use urt_gameplay::exit_rules::{TimerHit, TimerState, apply_timer};
|
| 502 |
let n = w[10] as usize;
|
crates/urt-gameplay/src/exit_rules.rs
CHANGED
|
@@ -16,6 +16,7 @@ pub struct Client {
|
|
| 16 |
pub enum Error {
|
| 17 |
InvalidScan,
|
| 18 |
InvalidEntity,
|
|
|
|
| 19 |
}
|
| 20 |
fn scan(clients: &[Client], count: usize) -> Result<&[Client], Error> {
|
| 21 |
if clients.len() > 64 || count > clients.len() {
|
|
@@ -184,9 +185,162 @@ pub fn apply_timer(
|
|
| 184 |
}
|
| 185 |
Ok(TimerOutcome { hit, resets })
|
| 186 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
#[cfg(test)]
|
| 188 |
mod tests {
|
| 189 |
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
#[test]
|
| 191 |
fn round_gate_bypasses_time_even_when_not_winning() {
|
| 192 |
let s = TimerState {
|
|
|
|
| 16 |
pub enum Error {
|
| 17 |
InvalidScan,
|
| 18 |
InvalidEntity,
|
| 19 |
+
InvalidState,
|
| 20 |
}
|
| 21 |
fn scan(clients: &[Client], count: usize) -> Result<&[Client], Error> {
|
| 22 |
if clients.len() > 64 || count > clients.len() {
|
|
|
|
| 185 |
}
|
| 186 |
Ok(TimerOutcome { hit, resets })
|
| 187 |
}
|
| 188 |
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
| 189 |
+
pub struct State {
|
| 190 |
+
pub blocked: bool,
|
| 191 |
+
pub pending: i32,
|
| 192 |
+
pub slow: bool,
|
| 193 |
+
pub predicate: bool,
|
| 194 |
+
pub timer: TimerState,
|
| 195 |
+
pub limits: LimitState,
|
| 196 |
+
}
|
| 197 |
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
| 198 |
+
pub enum Decision {
|
| 199 |
+
Blocked,
|
| 200 |
+
Wait,
|
| 201 |
+
Transition,
|
| 202 |
+
Class(usize),
|
| 203 |
+
Predicate,
|
| 204 |
+
Round,
|
| 205 |
+
Time,
|
| 206 |
+
Limit(LimitHit),
|
| 207 |
+
None,
|
| 208 |
+
}
|
| 209 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 210 |
+
pub struct Outcome {
|
| 211 |
+
pub decision: Decision,
|
| 212 |
+
pub resets: Vec<Reset>,
|
| 213 |
+
}
|
| 214 |
+
/// Complete recovered7805 policy order; supplied predicate and effect intents
|
| 215 |
+
/// deliberately do not implement native transitions, allocation or messages.
|
| 216 |
+
pub fn advance(
|
| 217 |
+
state: &mut State,
|
| 218 |
+
clients: &mut [Client],
|
| 219 |
+
entity_ids: &[u32],
|
| 220 |
+
) -> Result<Outcome, Error> {
|
| 221 |
+
if state.timer.mode != state.limits.mode {
|
| 222 |
+
return Err(Error::InvalidState);
|
| 223 |
+
}
|
| 224 |
+
scan(clients, state.limits.max_clients)?;
|
| 225 |
+
scan(clients, state.timer.client_count)?;
|
| 226 |
+
let simple = |decision| Outcome {
|
| 227 |
+
decision,
|
| 228 |
+
resets: vec![],
|
| 229 |
+
};
|
| 230 |
+
use crate::exit_prefix::Action;
|
| 231 |
+
match crate::exit_prefix::advance(
|
| 232 |
+
state.blocked,
|
| 233 |
+
&mut state.pending,
|
| 234 |
+
state.slow,
|
| 235 |
+
state.timer.now,
|
| 236 |
+
) {
|
| 237 |
+
Action::Blocked => return Ok(simple(Decision::Blocked)),
|
| 238 |
+
Action::Wait => return Ok(simple(Decision::Wait)),
|
| 239 |
+
Action::StartTransition => return Ok(simple(Decision::Transition)),
|
| 240 |
+
Action::ContinueModeRules => {}
|
| 241 |
+
}
|
| 242 |
+
if let Some(id) = class_guard(state.timer.mode, clients, state.limits.max_clients)? {
|
| 243 |
+
return Ok(simple(Decision::Class(id)));
|
| 244 |
+
}
|
| 245 |
+
if state.predicate {
|
| 246 |
+
return Ok(simple(Decision::Predicate));
|
| 247 |
+
}
|
| 248 |
+
let timed = apply_timer(state.timer, clients, entity_ids)?;
|
| 249 |
+
if let Some(hit) = timed.hit {
|
| 250 |
+
return Ok(Outcome {
|
| 251 |
+
decision: if hit == TimerHit::Round {
|
| 252 |
+
Decision::Round
|
| 253 |
+
} else {
|
| 254 |
+
Decision::Time
|
| 255 |
+
},
|
| 256 |
+
resets: timed.resets,
|
| 257 |
+
});
|
| 258 |
+
}
|
| 259 |
+
Ok(simple(
|
| 260 |
+
limit(state.limits, clients)?.map_or(Decision::None, Decision::Limit),
|
| 261 |
+
))
|
| 262 |
+
}
|
| 263 |
#[cfg(test)]
|
| 264 |
mod tests {
|
| 265 |
use super::*;
|
| 266 |
+
fn state() -> State {
|
| 267 |
+
State {
|
| 268 |
+
blocked: false,
|
| 269 |
+
pending: 0,
|
| 270 |
+
slow: false,
|
| 271 |
+
predicate: false,
|
| 272 |
+
timer: TimerState {
|
| 273 |
+
mode: 11,
|
| 274 |
+
time_limit: 1,
|
| 275 |
+
now: 60000,
|
| 276 |
+
client_count: 1,
|
| 277 |
+
..TimerState::default()
|
| 278 |
+
},
|
| 279 |
+
limits: LimitState {
|
| 280 |
+
mode: 11,
|
| 281 |
+
playing: 2,
|
| 282 |
+
score_limit: 10,
|
| 283 |
+
team_limit: 10,
|
| 284 |
+
scores: [20, 20],
|
| 285 |
+
max_clients: 1,
|
| 286 |
+
},
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
#[test]
|
| 290 |
+
fn complete_order_preserves_earlier_decision_priority() {
|
| 291 |
+
let mut c = [Client {
|
| 292 |
+
connection: 2,
|
| 293 |
+
class: 21,
|
| 294 |
+
..Client::default()
|
| 295 |
+
}];
|
| 296 |
+
let mut s = state();
|
| 297 |
+
s.blocked = true;
|
| 298 |
+
s.pending = 59000;
|
| 299 |
+
s.predicate = true;
|
| 300 |
+
assert_eq!(
|
| 301 |
+
advance(&mut s, &mut c, &[100]).unwrap().decision,
|
| 302 |
+
Decision::Blocked
|
| 303 |
+
);
|
| 304 |
+
assert_eq!(s.pending, 59000);
|
| 305 |
+
s.blocked = false;
|
| 306 |
+
assert_eq!(
|
| 307 |
+
advance(&mut s, &mut c, &[100]).unwrap().decision,
|
| 308 |
+
Decision::Transition
|
| 309 |
+
);
|
| 310 |
+
assert_eq!(s.pending, 0);
|
| 311 |
+
assert_eq!(
|
| 312 |
+
advance(&mut s, &mut c, &[100]).unwrap().decision,
|
| 313 |
+
Decision::Class(0)
|
| 314 |
+
);
|
| 315 |
+
c[0].class = 0;
|
| 316 |
+
assert_eq!(
|
| 317 |
+
advance(&mut s, &mut c, &[100]).unwrap().decision,
|
| 318 |
+
Decision::Predicate
|
| 319 |
+
);
|
| 320 |
+
s.predicate = false;
|
| 321 |
+
assert_eq!(
|
| 322 |
+
advance(&mut s, &mut c, &[100]).unwrap().decision,
|
| 323 |
+
Decision::Time
|
| 324 |
+
);
|
| 325 |
+
s.timer.time_limit = 0;
|
| 326 |
+
assert_eq!(
|
| 327 |
+
advance(&mut s, &mut c, &[100]).unwrap().decision,
|
| 328 |
+
Decision::Limit(LimitHit::HighFirst)
|
| 329 |
+
);
|
| 330 |
+
}
|
| 331 |
+
#[test]
|
| 332 |
+
fn inconsistent_mode_state_rejects_before_mutation() {
|
| 333 |
+
let mut s = state();
|
| 334 |
+
s.pending = 59000;
|
| 335 |
+
s.limits.mode = 9;
|
| 336 |
+
let before = s;
|
| 337 |
+
let mut clients = [Client::default()];
|
| 338 |
+
assert_eq!(
|
| 339 |
+
advance(&mut s, &mut clients, &[100]),
|
| 340 |
+
Err(Error::InvalidState)
|
| 341 |
+
);
|
| 342 |
+
assert_eq!(s, before);
|
| 343 |
+
}
|
| 344 |
#[test]
|
| 345 |
fn round_gate_bypasses_time_even_when_not_winning() {
|
| 346 |
let s = TimerState {
|
docs/FINDINGS.md
CHANGED
|
@@ -1496,3 +1496,6 @@ RE-813–822 replaces the scoreboard decimal assumption with actual37914 executi
|
|
| 1496 |
|
| 1497 |
|
| 1498 |
**RE-832:** Complete7805 execution now composes prefix, class, supplied predicate, round/time and limit policies with shared mutations across438 finite fixtures. [Evidence](research/QVM_EXIT_COMPLETE.md).
|
|
|
|
|
|
|
|
|
|
|
|
| 1496 |
|
| 1497 |
|
| 1498 |
**RE-832:** Complete7805 execution now composes prefix, class, supplied predicate, round/time and limit policies with shared mutations across438 finite fixtures. [Evidence](research/QVM_EXIT_COMPLETE.md).
|
| 1499 |
+
|
| 1500 |
+
|
| 1501 |
+
**RE-833:** Rust now composes the full recovered7805 priority and matches438 continuous source cases, including pending clears and mode9 reset/event intentions. [Evidence](research/RUST_EXIT_COMPLETE.md).
|
docs/PROVISIONAL_TODO.md
CHANGED
|
@@ -1509,3 +1509,9 @@ RE-831: Rust matches116 original timer cases, including active-round bypass, sig
|
|
| 1509 |
|
| 1510 |
|
| 1511 |
RE-832: Complete7805 execution now composes prefix, class, supplied predicate, round/time and limit policies with shared mutations across438 finite fixtures. Full runtime completion remains open. [Evidence](research/QVM_EXIT_COMPLETE.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1509 |
|
| 1510 |
|
| 1511 |
RE-832: Complete7805 execution now composes prefix, class, supplied predicate, round/time and limit policies with shared mutations across438 finite fixtures. Full runtime completion remains open. [Evidence](research/QVM_EXIT_COMPLETE.md).
|
| 1512 |
+
|
| 1513 |
+
|
| 1514 |
+
RE-833: Rust now composes the full recovered7805 priority and matches438 continuous source cases, including pending clears and mode9 reset/event intentions. Full runtime completion remains open. [Evidence](research/RUST_EXIT_COMPLETE.md).
|
| 1515 |
+
|
| 1516 |
+
|
| 1517 |
+
- [ ] RE-834: Recover supplied exit predicate/transition/native effects, compose ranking→exit→scoreboard state, and validate actual runtime traces and macOS/Linux rendering. [Batch scope](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
|
docs/RESEARCH_LEDGER.md
CHANGED
|
@@ -1830,3 +1830,12 @@ Rust matches116 original timer cases, including active-round bypass, signed mult
|
|
| 1830 |
## 2026-10-27 — RE-832 Continuous original exit-rule body with all recovered priority branches
|
| 1831 |
|
| 1832 |
Complete7805 execution now composes prefix, class, supplied predicate, round/time and limit policies with shared mutations across438 finite fixtures. Focused `cargo xtask test-group exit-complete` passes (5 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1830 |
## 2026-10-27 — RE-832 Continuous original exit-rule body with all recovered priority branches
|
| 1831 |
|
| 1832 |
Complete7805 execution now composes prefix, class, supplied predicate, round/time and limit policies with shared mutations across438 finite fixtures. Focused `cargo xtask test-group exit-complete` passes (5 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
|
| 1833 |
+
|
| 1834 |
+
|
| 1835 |
+
## 2026-10-27 — RE-833 Rust complete recovered exit-rule decision and mutation composition
|
| 1836 |
+
|
| 1837 |
+
Rust now composes the full recovered7805 priority and matches438 continuous source cases, including pending clears and mode9 reset/event intentions. Focused `cargo xtask test-group rust-exit-complete` passes (9 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
|
| 1838 |
+
|
| 1839 |
+
RE-833 final review: added five explicit source-to-Rust dependency edges and registry regression assertions; the targeted registry test and repeated `rust-exit-complete` focused group pass. Added the ten-task batch summary and RE-834 follow-up scope.
|
| 1840 |
+
|
| 1841 |
+
Batch RE-824–833 boundary: the single `cargo xtask test-full --since 1ed43d40` run passed after task ten: 1,628 Python tests, 127 Rust tests (assets 10, gameplay 89, xtask 28), 43 deterministic report producers, storage policy, 392 documentation files and whitespace checks. No generated report drift; validation notes amended into task ten.
|
docs/REVERSE_ENGINEERING_TODO.md
CHANGED
|
@@ -1546,3 +1546,9 @@ Purpose: turn the supplied installation into an evidence-backed specification of
|
|
| 1546 |
|
| 1547 |
|
| 1548 |
- [x] RE-832 Continuous original exit-rule body with all recovered priority branches ([evidence](research/QVM_EXIT_COMPLETE.md)).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1546 |
|
| 1547 |
|
| 1548 |
- [x] RE-832 Continuous original exit-rule body with all recovered priority branches ([evidence](research/QVM_EXIT_COMPLETE.md)).
|
| 1549 |
+
|
| 1550 |
+
|
| 1551 |
+
- [x] RE-833 Rust complete recovered exit-rule decision and mutation composition ([evidence](research/RUST_EXIT_COMPLETE.md)).
|
| 1552 |
+
|
| 1553 |
+
|
| 1554 |
+
- [ ] RE-834: Recover supplied exit predicate/transition/native effects, compose ranking→exit→scoreboard state, and validate actual runtime traces and macOS/Linux rendering. [Batch scope](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
|
docs/TESTING.md
CHANGED
|
@@ -128,3 +128,8 @@ Use `cargo xtask test-group <name>` with `coherent-memory`, `child-byte-effects`
|
|
| 128 |
## Original formatter and exit-rule groups
|
| 129 |
|
| 130 |
Use `cargo xtask test-group <name>` with `format-string`, `format-integer`, `formatter`, `scoreboard-formatter`, `rust-qvm-format`, `rust-scoreboard-original`, `format-wrappers`, `rust-format-wrappers`, `exit-prefix` or `rust-exit-prefix`. The registry regression verifies source-report selections for the new Rust parity producers and both new module paths. See [contract limits and corrected formatter assumptions](research/QVM_ORIGINAL_FORMATTER_BATCH.md). Full validation remains once after ten task commits.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
## Original formatter and exit-rule groups
|
| 129 |
|
| 130 |
Use `cargo xtask test-group <name>` with `format-string`, `format-integer`, `formatter`, `scoreboard-formatter`, `rust-qvm-format`, `rust-scoreboard-original`, `format-wrappers`, `rust-format-wrappers`, `exit-prefix` or `rust-exit-prefix`. The registry regression verifies source-report selections for the new Rust parity producers and both new module paths. See [contract limits and corrected formatter assumptions](research/QVM_ORIGINAL_FORMATTER_BATCH.md). Full validation remains once after ten task commits.
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
## Float formatter and complete exit-rule groups
|
| 134 |
+
|
| 135 |
+
Use `cargo xtask test-group <name>` with `format-float`, `rust-format-float`, `exit-class`, `rust-exit-class`, `exit-limits`, `rust-exit-limits`, `exit-timers`, `rust-exit-timers`, `exit-complete` or `rust-exit-complete`. The registry regression verifies each source-report dependency selects its Rust adapter and test. Scoped gameplay checks refresh test-count metadata. Keep focused validation before each commit and one full run after ten commits. See the [batch evidence and remaining effects](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
|
docs/research/QVM_FLOAT_EXIT_RULES_BATCH.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Float formatter and exit-rule composition: RE-824–833
|
| 2 |
+
|
| 3 |
+
Ten tasks recover original float formatting and compose the exit-rule body 7805 into Rust. These are bounded source contracts, not a running game or complete native parity.
|
| 4 |
+
|
| 5 |
+
| Task | Result |
|
| 6 |
+
| --- | --- |
|
| 7 |
+
| RE-824 | [Float formatter source arithmetic](QVM_FORMAT_FLOAT.md) |
|
| 8 |
+
| RE-825 | [Rust float formatter](RUST_FORMAT_FLOAT.md) |
|
| 9 |
+
| RE-826 | [Mode11 class guard](QVM_EXIT_CLASS.md) |
|
| 10 |
+
| RE-827 | [Rust class guard](RUST_EXIT_CLASS.md) |
|
| 11 |
+
| RE-828 | [Score/team exit limit priority](QVM_EXIT_LIMITS.md) |
|
| 12 |
+
| RE-829 | [Rust limit policy](RUST_EXIT_LIMITS.md) |
|
| 13 |
+
| RE-830 | [Round/time gates and mode 9 reset effects](QVM_EXIT_TIMERS.md) |
|
| 14 |
+
| RE-831 | [Rust timers and reset intentions](RUST_EXIT_TIMERS.md) |
|
| 15 |
+
| RE-832 | [Continuous exit-rule source composition](QVM_EXIT_COMPLETE.md) |
|
| 16 |
+
| RE-833 | [Rust complete recovered exit policy](RUST_EXIT_COMPLETE.md) |
|
| 17 |
+
|
| 18 |
+
## Evidence and boundaries
|
| 19 |
+
|
| 20 |
+
The float helper has 126 fixtures, plus 12 central formatter fixtures. It truncates repeated f32 fractional arithmetic; width applies to the integer portion. Negative zero loses its sign. Rust rejects nonfinite/out-of-domain values and bounds precision and width explicitly.
|
| 21 |
+
|
| 22 |
+
Exit class, limit and timer contracts have 45, 125 and 116 fixtures respectively. Continuous 7805 execution covers 438 cases, including conflicting priorities and shared client mutations. Rust matches those decisions, pending-state updates and reset intentions. The active round gate bypasses time-limit evaluation; the mode 9 path distinguishes collected client count from configured scan bounds.
|
| 23 |
+
|
| 24 |
+
Child 7662 remains a supplied predicate. Calls 7525/6825/7367, formatting, sending, logging and event allocation are captured or supplied effects. Event 69 intentions expose owner/time and flag OR 256; the source fixture supplies initial constructor flags 16. Neither adapter establishes the real allocator or transition implementation. Native/JIT execution, real game traces and unrestricted memory legality remain unverified.
|
| 25 |
+
|
| 26 |
+
## Validation
|
| 27 |
+
|
| 28 |
+
Focused cargo xtask groups passed before each task commit. Reports were regenerated twice with deterministic output, relevant Python and scoped Rust checks, storage policy, documentation links and whitespace validation. After commit ten, `cargo xtask test-full --since 1ed43d40` passed: 1,628 Python tests, 127 Rust tests (assets 10, gameplay 89, xtask 28), 43 affected producers regenerated twice deterministically, storage policy, 392 documentation files and whitespace checks. Generated reports left no uncommitted differences. This was the only full-suite invocation in the batch.
|
| 29 |
+
|
| 30 |
+
## Next work: RE-834
|
| 31 |
+
|
| 32 |
+
- [ ] Recover 7662 and replace the supplied predicate with original instruction evidence.
|
| 33 |
+
- [ ] Recover 7525/6825/7367 transition effects and their world/state ownership.
|
| 34 |
+
- [ ] Resolve formatting, copy, log, send and event constructors used by these paths.
|
| 35 |
+
- [ ] Integrate ranking, exit decisions and scoreboard publication across shared state.
|
| 36 |
+
- [ ] Audit legacy trace/memory assumptions and compare actual native/gameplay traces.
|
| 37 |
+
- [ ] Connect verified policies to the game runtime and validate macOS/Linux Bevy/Solari behavior.
|
docs/research/README.md
CHANGED
|
@@ -1086,3 +1086,8 @@ The extension collector adds five focused tests for native tail calls, command-r
|
|
| 1086 |
|
| 1087 |
|
| 1088 |
[RE-832: Continuous original exit-rule body with all recovered priority branches](QVM_EXIT_COMPLETE.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
|
| 1087 |
|
| 1088 |
[RE-832: Continuous original exit-rule body with all recovered priority branches](QVM_EXIT_COMPLETE.md).
|
| 1089 |
+
|
| 1090 |
+
|
| 1091 |
+
[RE-833: Rust complete recovered exit-rule decision and mutation composition](RUST_EXIT_COMPLETE.md).
|
| 1092 |
+
|
| 1093 |
+
- [RE-824–833 float formatter and exit-rule batch](QVM_FLOAT_EXIT_RULES_BATCH.md).
|
docs/research/RUST_EXIT_COMPLETE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rust complete recovered exit-rule decision and mutation composition
|
| 2 |
+
|
| 3 |
+
RE-833. Reproduce with `python3 scripts/analyze_rust_exit_complete.py`; focused validation: `cargo xtask test-group rust-exit-complete`. [Machine report](rust-exit-complete.json).
|
| 4 |
+
|
| 5 |
+
The typed state validates safe scan bounds and consistent mode inputs, then composes prefix, class, supplied predicate, timers and limit policies. Source parity checks every decision, offender, pending word, client field and reset intention, with an independent composition oracle. Unit tests stress conflicting priorities and rejection before mutation. Actual predicate/transition/allocation/log/send effects and full game runtime integration remain open.
|
| 6 |
+
|
| 7 |
+
Original executable/native runtime and complete Linux/macOS Rust/Bevy/Solari acceptance remain unverified.
|
docs/research/rust-exit-complete.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/research/rust-health-impulse.json
CHANGED
|
@@ -2985,7 +2985,7 @@
|
|
| 2985 |
"exit_code": 2
|
| 2986 |
}
|
| 2987 |
],
|
| 2988 |
-
"rust_unit_tests":
|
| 2989 |
"source_report": "docs/research/qvm-health-impulse.json",
|
| 2990 |
"source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
|
| 2991 |
}
|
|
|
|
| 2985 |
"exit_code": 2
|
| 2986 |
}
|
| 2987 |
],
|
| 2988 |
+
"rust_unit_tests": 89,
|
| 2989 |
"source_report": "docs/research/qvm-health-impulse.json",
|
| 2990 |
"source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
|
| 2991 |
}
|
scripts/analyze_rust_exit_complete.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import hashlib,json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from analyze_rust_exit_class import words
|
| 5 |
+
from analyze_rust_exit_limits import decode
|
| 6 |
+
from qvm_exit_rules_support import GLOBALS
|
| 7 |
+
from qvm_notification_support import CPOOL,POOL
|
| 8 |
+
from rust_lifecycle_support import binary,result
|
| 9 |
+
from analyze_qvm import write_json
|
| 10 |
+
|
| 11 |
+
def outcome(s):
|
| 12 |
+
req=s['requests'];a=s['input'];kind='none';id=-1
|
| 13 |
+
if not req:kind='wait'
|
| 14 |
+
elif req[0]['target']==7525:kind='blocked'
|
| 15 |
+
elif req[0]['target']==6825:kind='transition'
|
| 16 |
+
elif req[0]['target']==7367:kind='class';id=(req[1]['arguments']['20']-CPOOL-504)//6336
|
| 17 |
+
elif a['predicate']:kind='predicate'
|
| 18 |
+
elif len(req)>1:
|
| 19 |
+
if req[-1]['arguments']['8'] in [34577,34461]:kind='round' if req[-1]['arguments']['8']==34577 else 'time'
|
| 20 |
+
else:decoded=decode(req[1:]);kind=decoded['kind'];id=decoded['client']
|
| 21 |
+
resets=[]
|
| 22 |
+
for call,event,log in zip([r for r in req if r['target']==277695],s['events'],[r for r in req if r['target']==7259]):
|
| 23 |
+
entity=(call['arguments']['8']-POOL-24)//880;slot=next(j for j,c in enumerate(s['clients_before']) if c['140']==entity);resets.append({'client':slot,'entity':entity,'owner':event['fields']['140'],'time':event['fields']['84'],'kind':69,'flags_or':event['fields']['424']&256,'logged_value':log['arguments']['16']})
|
| 24 |
+
return {'kind':kind,'client':id,'pending':s['pending_after'],'clients':s['clients_after'],'resets':resets}
|
| 25 |
+
|
| 26 |
+
def main():
|
| 27 |
+
b=binary();p=Path('docs/research/qvm-exit-complete.json');data=p.read_bytes();rows=[]
|
| 28 |
+
for i,s in enumerate(json.loads(data)['probes']):
|
| 29 |
+
a=s['input'];c=s['clients_before'];args=[a[k] for k in GLOBALS]+[a['predicate'],len(c),64,*[v for j,c in enumerate(c) for v in words(c,j)],*[100+j for j in range(64)]];rows.append({'case':i,'source':outcome(s),'rust':result(b,'exit-complete',args)})
|
| 30 |
+
write_json(Path('docs/research/rust-exit-complete.json'),{'source_report':str(p),'source_report_sha256':hashlib.sha256(data).hexdigest(),'probes':rows,'limits':'Rust complete recovered7805 decision and shared mutation contract matches438 continuous original source fixtures.7662 predicate is supplied, event allocation and all transition/log/format/send effects remain intentions. Invalid raw pointer/count ABI and native/runtime acceptance remain open.'})
|
| 31 |
+
if __name__=='__main__':main()
|
scripts/test_rust_exit_complete.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib,json,unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from test_qvm_exit_complete import oracle,signed
|
| 4 |
+
from test_qvm_exit_limits import decision
|
| 5 |
+
class RustCompleteExitTests(unittest.TestCase):
|
| 6 |
+
def test_exact_source_outputs_and_independent_composition(self):
|
| 7 |
+
r=json.loads(Path('docs/research/rust-exit-complete.json').read_text());source=json.loads(Path(r['source_report']).read_text());self.assertEqual(len(r['probes']),438)
|
| 8 |
+
for p in r['probes']:
|
| 9 |
+
s=source['probes'][p['case']];pending,req,ids,kind=oracle(s['input'],s['clients_before']);self.assertEqual(p['rust'],p['source']);self.assertEqual(p['rust']['pending'],pending);self.assertEqual(p['rust']['kind'],kind);self.assertEqual(p['rust']['clients'],s['clients_after']);self.assertEqual(len(p['rust']['resets']),len(ids))
|
| 10 |
+
if kind=='client':self.assertEqual(p['rust']['client'],decision(s['input'],[{k:signed(v) for k,v in c.items()} for c in s['clients_before']])[1])
|
| 11 |
+
def test_source_identity(self):
|
| 12 |
+
r=json.loads(Path('docs/research/rust-exit-complete.json').read_text());self.assertEqual(hashlib.sha256(Path(r['source_report']).read_bytes()).hexdigest(),r['source_report_sha256'])
|
| 13 |
+
if __name__=='__main__':unittest.main()
|
xtask/checks.tsv
CHANGED
|
@@ -2136,3 +2136,21 @@ report scripts/analyze_qvm_exit_complete.py docs/research/qvm-exit-complete.json
|
|
| 2136 |
edge docs/research/qvm-exit-complete.json scripts/test_qvm_exit_complete.py
|
| 2137 |
group exit-complete scripts/analyze_qvm_exit_complete.py
|
| 2138 |
edge scripts/analyze_qvm_exit_complete.py scripts/analyze_qvm_resumer_audit.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2136 |
edge docs/research/qvm-exit-complete.json scripts/test_qvm_exit_complete.py
|
| 2137 |
group exit-complete scripts/analyze_qvm_exit_complete.py
|
| 2138 |
edge scripts/analyze_qvm_exit_complete.py scripts/analyze_qvm_resumer_audit.py
|
| 2139 |
+
edge Cargo.toml scripts/analyze_rust_exit_complete.py
|
| 2140 |
+
edge Cargo.lock scripts/analyze_rust_exit_complete.py
|
| 2141 |
+
edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_exit_complete.py
|
| 2142 |
+
edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_exit_complete.py
|
| 2143 |
+
edge crates/urt-gameplay/src/bin/urt-lifecycle.rs scripts/analyze_rust_exit_complete.py
|
| 2144 |
+
edge crates/urt-gameplay/src/exit_rules.rs scripts/analyze_rust_exit_complete.py
|
| 2145 |
+
edge crates/urt-gameplay/src/exit_prefix.rs scripts/analyze_rust_exit_complete.py
|
| 2146 |
+
group gameplay scripts/analyze_rust_exit_complete.py
|
| 2147 |
+
group rust-exit-complete scripts/analyze_rust_health_impulse.py
|
| 2148 |
+
report scripts/analyze_rust_exit_complete.py docs/research/rust-exit-complete.json
|
| 2149 |
+
edge docs/research/rust-exit-complete.json scripts/test_rust_exit_complete.py
|
| 2150 |
+
group rust-exit-complete scripts/analyze_rust_exit_complete.py
|
| 2151 |
+
edge scripts/analyze_rust_exit_complete.py scripts/analyze_qvm_resumer_audit.py
|
| 2152 |
+
edge docs/research/qvm-format-float.json scripts/analyze_rust_format_float.py
|
| 2153 |
+
edge docs/research/qvm-exit-class.json scripts/analyze_rust_exit_class.py
|
| 2154 |
+
edge docs/research/qvm-exit-limits.json scripts/analyze_rust_exit_limits.py
|
| 2155 |
+
edge docs/research/qvm-exit-timers.json scripts/analyze_rust_exit_timers.py
|
| 2156 |
+
edge docs/research/qvm-exit-complete.json scripts/analyze_rust_exit_complete.py
|
xtask/src/lib.rs
CHANGED
|
@@ -1098,6 +1098,11 @@ mod integration_tests {
|
|
| 1098 |
("qvm-scoreboard-formatter", "scoreboard_packet"),
|
| 1099 |
("qvm-format-wrappers", "format_wrappers"),
|
| 1100 |
("qvm-exit-prefix", "exit_prefix"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1101 |
] {
|
| 1102 |
let selected = plan(
|
| 1103 |
&g,
|
|
|
|
| 1098 |
("qvm-scoreboard-formatter", "scoreboard_packet"),
|
| 1099 |
("qvm-format-wrappers", "format_wrappers"),
|
| 1100 |
("qvm-exit-prefix", "exit_prefix"),
|
| 1101 |
+
("qvm-format-float", "format_float"),
|
| 1102 |
+
("qvm-exit-class", "exit_class"),
|
| 1103 |
+
("qvm-exit-limits", "exit_limits"),
|
| 1104 |
+
("qvm-exit-timers", "exit_timers"),
|
| 1105 |
+
("qvm-exit-complete", "exit_complete"),
|
| 1106 |
] {
|
| 1107 |
let selected = plan(
|
| 1108 |
&g,
|