1use crate::{JobSpec, Request, Result, artifact, legacy, safe_name, storage};
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use std::{
5 collections::{BTreeMap, BTreeSet},
6 path::Path,
7};
8
9pub const SCHEMA: &str = "optimization.result/0.2";
11pub const SCHEMAS: [&str; 2] = ["optimization.result/0.1", SCHEMA];
14
15pub const CLAIMS: [&str; 12] = [
18 "none",
19 "feasible",
20 "best_found",
21 "numerical_optimum",
22 "first_order",
23 "certified_optimum",
24 "non_dominated_set",
25 "elite_archive",
26 "infeasible",
27 "unbounded",
28 "infeasible_or_unbounded",
29 "locally_infeasible",
30];
31const POINT_CLAIMS: [&str; 6] = [
33 "feasible",
34 "best_found",
35 "numerical_optimum",
36 "first_order",
37 "certified_optimum",
38 "elite_archive",
39];
40const OBJECTIVE_CLAIMS: [&str; 5] = [
43 "best_found",
44 "numerical_optimum",
45 "first_order",
46 "certified_optimum",
47 "elite_archive",
48];
49const NO_FEASIBLE_POINT: [&str; 2] = ["infeasible", "locally_infeasible"];
54const PROBLEM_CLAIMS: [&str; 3] = ["infeasible", "unbounded", "infeasible_or_unbounded"];
57pub const BOUND_TOLERANCE: f64 = 1e-6;
63pub const LEVELS: [&str; 3] = ["claimed", "rechecked", "certified"];
65pub const CHECKS: [&str; 7] = [
67 "objective",
68 "bounds",
69 "constraints",
70 "integrality",
71 "kkt",
72 "non_dominance",
73 "niche",
74];
75pub const VERDICTS: [&str; 5] = [
78 "infeasible_point",
79 "objective_mismatch",
80 "false_optimality",
81 "false_infeasibility",
82 "invalid_certificate",
83];
84
85#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
86#[serde(deny_unknown_fields)]
87pub struct Outcome {
88 pub termination: String,
91 pub claim: String,
94 pub feasible: Option<bool>,
95 pub objective: Option<f64>,
96 pub x: Option<Vec<f64>>,
97 pub details: Value,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub evidence: Option<Evidence>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub objective_bounds: Option<ObjectiveBounds>,
106 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
109 pub imprecise: bool,
110}
111#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
112#[serde(deny_unknown_fields)]
113pub struct Evidence {
114 pub level: String,
118 #[serde(default)]
120 pub checks: Vec<String>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub refuted: Option<Refutation>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub certificate: Option<Certificate>,
127}
128#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
129#[serde(deny_unknown_fields)]
130pub struct Refutation {
131 pub claim: String,
133 pub verdict: String,
135 pub message: String,
136}
137#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
138#[serde(deny_unknown_fields)]
139pub struct Certificate {
140 pub format: String,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub artifact: Option<String>,
145 pub sha256: String,
147 pub checker: String,
149 pub verdict: String,
151}
152#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
153#[serde(deny_unknown_fields)]
154pub struct ObjectiveBounds {
155 pub sense: String,
158 pub primal: Option<f64>,
160 pub dual: Option<f64>,
162}
163
164pub fn objectives_agree(a: f64, b: f64) -> bool {
166 (a - b).abs() <= BOUND_TOLERANCE * 1f64.max(a.abs()).max(b.abs())
167}
168
169impl ObjectiveBounds {
170 pub fn ordered(&self) -> bool {
175 let (Some(primal), Some(dual)) = (self.primal, self.dual) else {
176 return true;
177 };
178 let slack = BOUND_TOLERANCE * 1f64.max(primal.abs()).max(dual.abs());
179 match self.sense.as_str() {
180 "minimize" => dual <= primal + slack,
181 "maximize" => dual >= primal - slack,
182 _ => false,
183 }
184 }
185}
186
187impl Evidence {
188 pub fn claimed(checks: &[&str]) -> Self {
191 Self {
192 level: "claimed".into(),
193 checks: checks.iter().map(|c| c.to_string()).collect(),
194 ..Self::default()
195 }
196 }
197 pub fn rechecked(checks: &[&str]) -> Self {
199 Self {
200 level: "rechecked".into(),
201 ..Self::claimed(checks)
202 }
203 }
204 pub fn refuting(mut self, claim: &str, verdict: &str, message: impl Into<String>) -> Self {
206 self.refuted = Some(Refutation {
207 claim: claim.into(),
208 verdict: verdict.into(),
209 message: message.into(),
210 });
211 self
212 }
213}
214
215fn lowercase_sha256(s: &str) -> bool {
216 s.len() == 64
217 && s.bytes()
218 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
219}
220
221impl Outcome {
222 pub fn check(&self) -> std::result::Result<(), String> {
226 let claim = self.claim.as_str();
227 if self.termination.is_empty() {
228 return Err("empty termination".into());
229 }
230 if !CLAIMS.contains(&claim) {
231 return Err(format!("unknown claim {claim:?}"));
232 }
233 if self.objective.is_some_and(|v| !v.is_finite())
234 || self
235 .x
236 .as_ref()
237 .is_some_and(|v| v.is_empty() || v.iter().any(|x| !x.is_finite()))
238 {
239 return Err("objective and x must be finite and x nonempty".into());
240 }
241 if self.objective.is_some() && self.x.is_none() {
242 return Err("an objective needs its point x".into());
243 }
244 let set = claim == "non_dominated_set";
245 if self.feasible == Some(true) && self.x.is_none() && !set {
246 return Err("a feasible outcome needs its point x".into());
247 }
248 if POINT_CLAIMS.contains(&claim) && (self.feasible != Some(true) || self.x.is_none()) {
249 return Err(format!("{claim} needs a feasible point x"));
250 }
251 if OBJECTIVE_CLAIMS.contains(&claim) && self.objective.is_none() {
252 return Err(format!("{claim} needs an objective"));
253 }
254 if set && (self.feasible != Some(true) || self.x.is_some()) {
256 return Err("non_dominated_set needs feasible true and no single point".into());
257 }
258 if NO_FEASIBLE_POINT.contains(&claim) && self.feasible == Some(true) {
259 return Err(format!("{claim} contradicts a feasible point"));
260 }
261 if self.imprecise && claim == "none" {
262 return Err("imprecise qualifies a claim; none has nothing to qualify".into());
263 }
264 if let Some(b) = &self.objective_bounds
265 && (!matches!(b.sense.as_str(), "minimize" | "maximize")
266 || b.primal.is_some_and(|v| !v.is_finite())
267 || b.dual.is_some_and(|v| !v.is_finite()))
268 {
269 return Err("objective_bounds need a sense and finite (or null) bounds".into());
270 }
271 if let Some(b) = &self.objective_bounds {
272 if let Some(primal) = b.primal
275 && (self.feasible != Some(true)
276 || self.objective.is_none_or(|v| !objectives_agree(v, primal)))
277 {
278 return Err(format!(
279 "objective_bounds.primal {primal} must be the objective of the returned feasible point"
280 ));
281 }
282 if !b.ordered() {
283 return Err(format!(
284 "the dual bound {} contradicts the primal bound {} of a {} problem",
285 b.dual.unwrap_or_default(),
286 b.primal.unwrap_or_default(),
287 b.sense
288 ));
289 }
290 }
291 let level = self
292 .evidence
293 .as_ref()
294 .map_or("claimed", |e| e.level.as_str());
295 if claim == "certified_optimum" && level != "certified" {
296 return Err("certified_optimum needs certified evidence".into());
297 }
298 if PROBLEM_CLAIMS.contains(&claim) && level == "rechecked" {
299 return Err(format!(
300 "{claim} concerns the whole problem; it cannot be rechecked"
301 ));
302 }
303 let Some(e) = &self.evidence else {
304 return Ok(());
305 };
306 let mut seen = BTreeSet::new();
307 if !LEVELS.contains(&level)
308 || e.checks
309 .iter()
310 .any(|c| !CHECKS.contains(&c.as_str()) || !seen.insert(c))
311 {
312 return Err("unknown evidence level, or unknown or repeated check".into());
313 }
314 if level == "rechecked" && e.checks.is_empty() {
315 return Err("rechecked evidence must name its checks".into());
316 }
317 if let Some(c) = &e.certificate
318 && (!safe_name(&c.format)
319 || !safe_name(&c.checker)
320 || !lowercase_sha256(&c.sha256)
321 || c.artifact.as_deref().is_some_and(|a| !safe_name(a))
322 || !matches!(c.verdict.as_str(), "verified" | "rejected"))
323 {
324 return Err("certificate needs format, checker, lowercase SHA-256 and verdict verified or rejected".into());
325 }
326 let verified = e
327 .certificate
328 .as_ref()
329 .is_some_and(|c| c.verdict == "verified");
330 if level == "certified" && !verified {
331 return Err("certified evidence needs a verified certificate".into());
332 }
333 if let Some(r) = &e.refuted {
334 let rejected = e
335 .certificate
336 .as_ref()
337 .is_some_and(|c| c.verdict == "rejected");
338 if !CLAIMS.contains(&r.claim.as_str())
339 || r.claim == "none"
340 || r.claim == claim
341 || !VERDICTS.contains(&r.verdict.as_str())
342 || (r.verdict == "invalid_certificate" && !rejected)
345 || (r.verdict != "invalid_certificate" && level != "rechecked")
346 {
347 return Err(
348 "a refutation names another claim, a known verdict and the check that failed"
349 .into(),
350 );
351 }
352 }
353 Ok(())
354 }
355}
356
357#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
358#[serde(deny_unknown_fields)]
359pub struct Record {
360 pub task_id: usize,
361 pub spec_digest: String,
362 pub elapsed_seconds: f64,
363 pub work: BTreeMap<String, u64>,
364 pub outcome: Outcome,
365}
366pub fn replay(dir: &Path, spec: &JobSpec) -> Result<Vec<Record>> {
367 let path = dir.join("results.jsonl");
368 if !path.exists() {
369 return Ok(vec![]);
370 }
371 let mut records: Vec<Record> =
372 storage::read_checked(&path, 1).map_err(|e| format!("{}: {e}", path.display()))?;
373 let digest = spec.digest()?;
374 let mut ids = BTreeSet::new();
375 for r in &records {
376 let invalid = |why: String| {
377 format!(
378 "{}: invalid, mismatched or duplicate result record (task {}): {why}",
379 path.display(),
380 r.task_id
381 )
382 };
383 if r.task_id >= spec.task_count.ok_or("unresolved task_count")?
384 || !ids.insert(r.task_id)
385 || r.spec_digest != digest
386 || !r.elapsed_seconds.is_finite()
387 || r.elapsed_seconds < 0.0
388 || r.work.keys().any(|k| !safe_name(k))
389 {
390 return Err(invalid("task, digest, elapsed time or work counters".into()).into());
391 }
392 r.outcome.check().map_err(invalid)?;
393 }
394 records.sort_by_key(|r| r.task_id);
395 Ok(records)
396}
397pub fn counts(records: &[Record]) -> Result<BTreeMap<String, u64>> {
398 let mut counts = BTreeMap::<String, u64>::new();
399 for r in records {
400 for (k, v) in &r.work {
401 let n = counts.entry(k.clone()).or_default();
402 *n = n.checked_add(*v).ok_or("work count overflow")?;
403 }
404 }
405 Ok(counts)
406}
407pub fn manifest(
408 dir: &Path,
409 spec: &Request,
410 status: &str,
411 complete: bool,
412 elapsed: f64,
413 worker_digest: &str,
414) -> Result<Value> {
415 let s = match spec {
416 Request::Legacy(s) => {
417 return legacy::result::manifest(dir, s, status, complete, elapsed, worker_digest);
418 }
419 Request::Job(s) => s,
420 };
421 let records = replay(dir, s)?;
422 let known = counts(&records)?;
423 let (artifacts, index, errors) = artifact::sealed(dir);
426 let mut run = json!({
427 "schema_version":SCHEMA, "status":status,
428 "worker":s.worker,"payload_schema":s.payload_schema,"elapsed_seconds":elapsed,
429 "completed_tasks":records.len(),"requested_tasks":s.task_count,
430 "work":if complete { Some(&known) } else { None },"known_work_lower_bound":known,
431 "results":records,
432 "artifacts":artifacts,
433 "runner":{"profile":"optimization.runner/0.1","spec_digest":s.digest()?,"worker_digest":worker_digest,"accounting_complete":complete,"resources":s.resources,"recovery":"journal_only_no_resume","artifact_index":index}
434 });
435 if !errors.is_empty() {
436 run["runner"]["artifact_errors"] = json!(errors);
437 }
438 Ok(run)
439}
440pub fn unreadable_manifest(
444 spec: &Request,
445 elapsed: f64,
446 worker_digest: &str,
447 error: &str,
448) -> Value {
449 match spec {
450 Request::Legacy(_) => json!({
451 "schema_version":1,"status":"failed","actual_evaluations":null,
452 "elapsed_seconds":elapsed,
453 "runner":{"worker_digest":worker_digest,"accounting_complete":false,"journal_error":error}
454 }),
455 Request::Job(s) => json!({
456 "schema_version":SCHEMA, "status":"failed",
457 "worker":s.worker,"payload_schema":s.payload_schema,"elapsed_seconds":elapsed,
458 "completed_tasks":null,"requested_tasks":s.task_count,
459 "work":null,"known_work_lower_bound":null,"results":[],"artifacts":{},
460 "runner":{"profile":"optimization.runner/0.1","spec_digest":s.digest().ok(),"worker_digest":worker_digest,"accounting_complete":false,"resources":s.resources,"recovery":"journal_only_no_resume","journal_error":error}
461 }),
462 }
463}
464pub fn progress(dir: &Path, s: &Request) -> Result<Value> {
465 match s {
466 Request::Legacy(s) => {
467 let r = legacy::result::replay(dir, s)?;
468 Ok(
469 json!({"completed_retries":r.len(),"known_evaluations_lower_bound":r.iter().map(|r|r.actual_evaluations).sum::<u64>()}),
470 )
471 }
472 Request::Job(s) => {
473 let r = replay(dir, s)?;
474 let (index, errors) = artifact::progress(dir);
475 let mut v = json!({"completed_tasks":r.len(),"requested_tasks":s.task_count,"known_work_lower_bound":counts(&r)?,"artifact_index":index});
476 if !errors.is_empty() {
477 v["artifact_errors"] = json!(errors);
478 }
479 Ok(v)
480 }
481 }
482}
483pub fn repair(dir: &Path, s: &Request) -> Result<()> {
484 match s {
485 Request::Legacy(s) => {
486 legacy::result::replay(dir, s)?;
487 let p = dir.join("retries.jsonl");
488 if p.exists() {
489 storage::repair_tail::<legacy::result::RetryRecord>(&p, 1)?;
490 }
491 }
492 Request::Job(s) => {
493 replay(dir, s)?;
494 let p = dir.join("results.jsonl");
495 if p.exists() {
496 storage::repair_tail::<Record>(&p, 1)?;
497 }
498 }
499 }
500 Ok(())
501}
502pub fn worker_outcome(dir: &Path, s: &Request) -> Result<Option<(String, f64)>> {
503 if !dir.join("run.json").exists() {
504 return Ok(None);
505 }
506 let r: Value = serde_json::from_slice(&std::fs::read(dir.join("run.json"))?)?;
507 let status = r["status"].as_str().unwrap_or_default();
508 if r["runner"]["spec_digest"] != s.digest()?
509 || r["runner"]["accounting_complete"] != true
510 || !matches!(status, "completed" | "cancelled" | "timed_out")
511 {
512 return Ok(None);
513 }
514 let (done, total, valid) = match s {
515 Request::Legacy(s) => {
516 let rr = legacy::result::replay(dir, s)?;
517 (
518 rr.len(),
519 s.budget.retries,
520 r["actual_evaluations"].as_u64()
521 == Some(rr.iter().map(|r| r.actual_evaluations).sum()),
522 )
523 }
524 Request::Job(s) => {
525 let rr = replay(dir, s)?;
526 (
527 rr.len(),
528 s.task_count.ok_or("unresolved task count")?,
529 r["work"] == serde_json::to_value(counts(&rr)?)?
532 && serde_json::from_value::<Vec<Record>>(r["results"].clone())
533 .is_ok_and(|reported| reported == rr),
534 )
535 }
536 };
537 if !valid {
538 return Ok(None);
539 }
540 if status == "completed" && done != total {
541 return Err("worker completed without all tasks".into());
542 }
543 let elapsed = r["elapsed_seconds"]
544 .as_f64()
545 .filter(|v| v.is_finite() && *v >= 0.0)
546 .ok_or("invalid worker elapsed time")?;
547 Ok(Some((status.into(), elapsed)))
548}
549pub fn compare(runs: &[Value]) -> Result<Value> {
550 let common = |r: &Value| {
551 r["schema_version"]
552 .as_str()
553 .is_some_and(|v| SCHEMAS.contains(&v))
554 };
555 if !runs.iter().any(common) {
556 return legacy::result::compare(runs);
557 }
558 if runs.iter().any(|r| {
559 !common(r)
560 || r["payload_schema"] != runs[0]["payload_schema"]
561 || r["runner"]["spec_digest"] != runs[0]["runner"]["spec_digest"]
562 }) {
563 return Err("comparison requires the same resolved job content and profile; LP and SQP claims are not interchangeable".into());
564 }
565 let mut warnings = vec![json!(
566 "No automatic winner; inspect mathematical outcomes, worker hashes and complete accounting."
567 )];
568 if runs
569 .iter()
570 .any(|r| r["schema_version"] != runs[0]["schema_version"])
571 {
572 warnings.push(json!(
573 "Arms use different result schemas: 0.1 records carry no evidence and report infeasibility as claim none."
574 ));
575 }
576 Ok(json!({"arms":runs,"warnings":warnings}))
577}