///|
priv struct SavedFile {
path : String
bytes : Array[Int]
} derive(ToJson, @json.FromJson)
///|
priv struct Checkpoint {
score_mode : String
names : Array[String]
policies : Array[Int]
consecutive : Bool
baseline_trials : Array[Bool?]
schema_version : Int
engine_version : String
original_key : String
context : String
targets : Array[String]
files : Array[SavedFile]
state : Int
pass_index : Int
sequence : Int
evaluations : Int
hits : Int
baseline : Int
finals : Int
status : String
cache : Array[(String, Bool)]
} derive(ToJson, @json.FromJson)
///|
/// Serialize a settled batch boundary. The caller supplies a canonical context
/// containing predicate configuration and toolchain identity. Pending work must
/// finish or use the previously saved boundary; it is never marked evaluated.
pub fn Session::checkpoint(
self : Session,
original : @manifest.Snapshot,
context : String,
) -> Result[Json, String] {
if !self.pending.is_empty() {
return Err("MR_CHECKPOINT_PENDING: settle batch before saving")
}
let saved : Checkpoint = {
score_mode: self.score_mode,
names: self.names.copy(),
policies: [
self.candidate_policy.repeat,
self.candidate_policy.quorum,
self.final_policy.repeat,
self.final_policy.quorum,
self.command_cost,
],
consecutive: self.candidate_policy.consecutive,
baseline_trials: self.baseline_trials.copy(),
schema_version: 4,
engine_version: "project-v4",
original_key: original.key(),
context,
targets: self.targets.copy(),
files: self.best
.files()
.map(f => { path: f.0, bytes: f.1.iter().map(b => b.to_int()).to_array() }),
state: self.state,
pass_index: self.pass_index,
sequence: self.sequence,
evaluations: self.evaluations,
hits: self.hits,
baseline: self.baseline,
finals: self.finals,
status: self.status.to_json().stringify(),
cache: self.cache.iter().to_array(),
}
Ok(ToJson::to_json(saved))
}
///|
/// Restore validated state and rebuild its lazy pass iterator. Completed cache
/// entries suppress re-execution; final verification is always performed anew.
/// The supplied total budget may be enlarged, but must cover work already done.
pub fn Session::restore(
document : Json,
original : @manifest.Snapshot,
includes : Array[String],
protects : Array[String],
ignores : Array[String],
context : String,
max_tests? : Int = 2500,
repeat? : Int = 1,
quorum? : Int = 1,
consecutive? : Bool = false,
final_repeat? : Int = 3,
final_quorum? : Int = 3,
command_cost? : Int = 1,
score? : String = "bytes",
pass_include? : Array[String] = [],
pass_exclude? : Array[String] = [],
extra_passes? : Array[&@pass_api.ReductionPass] = [],
) -> Result[Session, String] {
let saved : Checkpoint = @json.from_json(document) catch {
_ => return Err("MR_CHECKPOINT_FORMAT: malformed checkpoint")
}
if saved.schema_version != 4 || saved.engine_version != "project-v4" {
return Err("MR_CHECKPOINT_VERSION: incompatible checkpoint")
}
if saved.original_key != original.key() || saved.context != context {
return Err(
"MR_CHECKPOINT_CONTEXT: input, configuration or toolchain changed",
)
}
let session = match
Session::new(
original,
includes,
protects,
ignores,
max_tests~,
repeat~,
quorum~,
consecutive~,
final_repeat~,
final_quorum~,
command_cost~,
score~,
pass_include~,
pass_exclude~,
extra_passes~,
) {
Ok(s) => s
Err(e) => return Err(e)
}
if saved.policies !=
[repeat, quorum, final_repeat, final_quorum, command_cost] ||
saved.consecutive != consecutive {
return Err("MR_CHECKPOINT_CONTEXT: stability policy changed")
}
if saved.score_mode != score || saved.names != session.names {
return Err("MR_CHECKPOINT_CONTEXT: score or passes changed")
}
if saved.targets != session.targets {
return Err("MR_CHECKPOINT_CONTEXT: selection changed")
}
if saved.state < 0 ||
saved.state > 3 ||
saved.pass_index < 0 ||
saved.pass_index > session.names.length() ||
saved.evaluations < 0 ||
saved.evaluations > max_tests - final_repeat * command_cost ||
saved.sequence < 0 ||
saved.sequence > 100000000 ||
saved.hits < 0 ||
saved.baseline < 0 ||
saved.baseline > session.baseline_policy.repeat ||
saved.finals < 0 ||
saved.finals > final_repeat ||
saved.cache.length() > saved.evaluations ||
saved.files.length() > 1000 {
return Err("MR_CHECKPOINT_STATE: invalid state or insufficient budget")
}
if saved.baseline_trials.length() > session.baseline_policy.repeat ||
saved.baseline_trials.filter(v => v == Some(true)).length() !=
saved.baseline {
return Err("MR_CHECKPOINT_STATE: invalid baseline evidence")
}
let baseline_complete = session.baseline_policy.decide(saved.baseline_trials) ==
Interesting
if !baseline_complete &&
max_tests <
saved.evaluations +
(session.baseline_policy.repeat + final_repeat) * command_cost {
return Err("MR_CHECKPOINT_STATE: insufficient baseline retry budget")
}
let files : Array[(String, Bytes)] = []
let mut size = 0
for f in saved.files {
size += f.bytes.length()
if f.bytes.length() > 1048576 || size > 16777216 {
return Err("MR_CHECKPOINT_SIZE: snapshot exceeds limits")
}
let bytes : Array[Byte] = Array::make(f.bytes.length(), 0)
for i in 0.. 255 {
return Err("MR_CHECKPOINT_FORMAT: invalid byte")
}
bytes[i] = f.bytes[i].to_byte()
}
files.push((f.path, Bytes::from_array(bytes)))
}
let best = match @manifest.Snapshot::new(files) {
Ok(s) => s
Err(e) => return Err(e)
}
let initial = original.files()
for f in best.files() {
if !initial.iter().any(o => o.0 == f.0) {
return Err("MR_CHECKPOINT_PATH: unknown file")
}
}
for f in initial {
if !session.targets.contains(f.0) && !best.files().contains(f) {
return Err("MR_CHECKPOINT_PROTECTED: protected content changed")
}
}
if best.key() != original.key() && !better(score, best, original) {
return Err("MR_CHECKPOINT_SCORE: snapshot is not reduced")
}
session.best = best
session.pass_index = saved.pass_index
session.sequence = saved.sequence
session.evaluations = saved.evaluations
session.hits = saved.hits
session.baseline = saved.baseline
session.finals = 0
// Interrupted baseline is repeated; completed baseline is preserved.
session.state = if !baseline_complete {
0
} else if saved.state == 2 {
2
} else if saved.state == 3 &&
saved.status == @model.RunStatus::Completed.to_json().stringify() {
2
} else {
1
}
if session.state == 0 {
session.baseline = 0
} else {
session.baseline_trials.append(saved.baseline_trials)
}
for entry in saved.cache {
if session.cache.contains(entry.0) {
return Err("MR_CHECKPOINT_CACHE: duplicate key")
}
session.cache[entry.0] = entry.1
}
Ok(session)
}