///|
/// Synchronous pure-driver evaluation contract. Each call is one uncached trial.
/// Return Unresolved for inconclusive observations, Err for execution failures.
pub(open) trait CandidateEvaluator {
fn evaluate(Self, @manifest.Snapshot) -> Result[@stability.Decision, String]
}
///|
/// Immutable versioned observation delivered in deterministic candidate order.
pub(all) struct Event {
schema_version : Int
candidate_id : Int
phase : String
trial : Int
decision : @stability.Decision
} derive(ToJson)
///|
/// Event delivery is synchronous; sinks may record evidence or update a UI.
pub(open) trait EventSink {
fn emit(Self, Event) -> Unit
}
///|
priv struct NoEvents {}
///|
impl EventSink for NoEvents with fn emit(_, _) {
()
}
///|
/// Pure reduction options. Native CLI time/process controls belong to app.Config.
pub(all) struct ReduceConfig {
max_tests : Int
repeat : Int
quorum : Int
consecutive : Bool
final_repeat : Int
final_quorum : Int
score : String
includes : Array[String]
protects : Array[String]
ignores : Array[String]
pass_include : Array[String]
pass_exclude : Array[String]
extra_passes : Array[&@pass_api.ReductionPass]
}
///|
/// Defaults match the deterministic project engine: 2 baseline, 3 final trials.
pub fn default_config() -> ReduceConfig {
{
max_tests: 2500,
repeat: 1,
quorum: 1,
consecutive: false,
final_repeat: 3,
final_quorum: 3,
score: "bytes",
includes: [],
protects: [],
ignores: [],
pass_include: [],
pass_exclude: [],
extra_passes: [],
}
}
///|
/// Built-in pure passes, in the same order as the native project CLI.
pub fn default_passes() -> Array[&@pass_api.ReductionPass] {
@project.default_passes()
}
///|
fn session(
original : @manifest.Snapshot,
c : ReduceConfig,
) -> Result[@project.Session, String] {
@project.Session::new(
original,
c.includes,
c.protects,
c.ignores,
max_tests=c.max_tests,
repeat=c.repeat,
quorum=c.quorum,
consecutive=c.consecutive,
final_repeat=c.final_repeat,
final_quorum=c.final_quorum,
score=c.score,
pass_include=c.pass_include,
pass_exclude=c.pass_exclude,
extra_passes=c.extra_passes,
)
}
///|
fn drive(
s : @project.Session,
evaluator : &CandidateEvaluator,
c : ReduceConfig,
sink : &EventSink,
baseline_only : Bool,
) -> Result[Unit, String] {
while s.phase() != "stopped" {
if baseline_only && s.phase() != "baseline" {
break
}
let batch = s.next_batch()
if batch.is_empty() {
break
}
let phase = s.phase()
for candidate in batch {
let trials : Array[Bool?] = []
let count = if phase == "reducing" { c.repeat } else { 1 }
for trial in 0.. d
Err(e) => return Err(e)
}
sink.emit({
schema_version: 1,
candidate_id: candidate.id,
phase,
trial: trial + 1,
decision,
})
trials.push(
match decision {
Interesting => Some(true)
Uninteresting => Some(false)
Unresolved => None
},
)
}
let matched = if phase == "reducing" {
let policy = @stability.Policy::new(
c.repeat,
c.quorum,
consecutive=c.consecutive,
).unwrap()
match policy.decide(trials) {
Interesting => Some(true)
Uninteresting => Some(false)
Unresolved => None
}
} else {
trials[0]
}
match s.record_result(candidate.id, matched, cost=count) {
Err(e) => return Err(e)
Ok(_) => ()
}
}
}
Ok(())
}
///|
/// Validate only the baseline; no pass runs and no original data is changed.
pub fn check(
original : @manifest.Snapshot,
evaluator : &CandidateEvaluator,
config? : ReduceConfig = default_config(),
sink? : &EventSink = NoEvents::{ },
) -> Result[@stability.Statistics, String] {
let s = match session(original, config) {
Ok(s) => s
Err(e) => return Err(e)
}
match drive(s, evaluator, config, sink, true) {
Err(e) => Err(e)
Ok(_) => Ok(s.baseline_statistics())
}
}
///|
/// Reduce immutable input using the same cached deterministic engine as the CLI.
/// Baseline and final trials always bypass the candidate cache.
pub fn reduce(
original : @manifest.Snapshot,
evaluator : &CandidateEvaluator,
config? : ReduceConfig = default_config(),
sink? : &EventSink = NoEvents::{ },
) -> Result[@project.ResultSummary, String] {
let s = match session(original, config) {
Ok(s) => s
Err(e) => return Err(e)
}
match drive(s, evaluator, config, sink, false) {
Err(e) => Err(e)
Ok(_) => Ok(s.result())
}
}