Skip to main content

forge_engine/lab/
archive.rs

1use crate::config::ForgeConfig;
2use crate::error::ForgeResult;
3use crate::lab::evaluate::ScoreVector;
4use crate::runtime::novelty;
5use crate::runtime::patch::types::StructuredPatch;
6use crate::store::ForgeStore;
7
8/// Result of an archive insertion attempt.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum ArchiveUpdate {
11    Inserted,
12    Replaced,
13    BelowGate,
14    NoChange,
15}
16
17/// Compute the cell key for a candidate.
18pub fn compute_cell_key(
19    score_vector: &ScoreVector,
20    patch: &StructuredPatch,
21    config: &ForgeConfig,
22) -> String {
23    let novelty_bin = compute_novelty_bin(score_vector.novelty, config);
24    let stability_bin = compute_stability_bin(score_vector.stability, config);
25    let tags = novelty::extract_strategy_tags(patch);
26    let approach_family = novelty::determine_approach_family(&tags);
27
28    format!("{novelty_bin}:{stability_bin}:{approach_family}")
29}
30
31/// Determine novelty bin from score.
32fn compute_novelty_bin(novelty: f64, config: &ForgeConfig) -> String {
33    for bin in &config.lab.archive.novelty_bins {
34        if novelty >= bin.lo && novelty < bin.hi {
35            return bin.name.clone();
36        }
37    }
38    // Edge case: exactly 1.0
39    if let Some(last) = config.lab.archive.novelty_bins.last() {
40        if (novelty - last.hi).abs() < f64::EPSILON {
41            return last.name.clone();
42        }
43    }
44    "low".to_string()
45}
46
47/// Determine stability bin.
48fn compute_stability_bin(_stability: f64, _config: &ForgeConfig) -> &'static str {
49    // Single-run tasks use "stable" by default
50    "stable"
51}
52
53/// Compare two candidates for archive dominance.
54/// Uses fixed comparison weights — NOT the AlgebraSpec's operator_weights.
55/// Rationale: archive ranking is cross-spec; it needs a stable comparator.
56pub fn compute_score_summary(scores: &ScoreVector) -> f64 {
57    // These weights are intentionally fixed for archive comparison consistency.
58    // AlgebraSpec.operator_weights govern generation; these govern selection.
59    0.70 * scores.correctness + 0.20 * scores.novelty + 0.10 * scores.stability
60}
61
62/// Insert a candidate into the archive if it qualifies.
63pub fn archive_insert(
64    store: &ForgeStore,
65    candidate_id: &str,
66    scores: &ScoreVector,
67    patch: &StructuredPatch,
68    config: &ForgeConfig,
69    cea_fingerprint: Option<&str>,
70) -> ForgeResult<ArchiveUpdate> {
71    // Correctness gate
72    if scores.correctness < config.lab.archive.correctness_gate {
73        return Ok(ArchiveUpdate::BelowGate);
74    }
75
76    let cell_key = compute_cell_key(scores, patch, config);
77    let score_summary = compute_score_summary(scores);
78
79    // Check existing cell
80    let existing = store.get_archive_cell(&cell_key)?;
81
82    match existing {
83        Some(cell) => {
84            let existing_summary: f64 =
85                serde_json::from_str(&cell.score_summary_json).unwrap_or(0.0);
86
87            if score_summary > existing_summary {
88                let summary_json = serde_json::to_string(&score_summary)?;
89                store.upsert_archive_cell(
90                    &cell_key,
91                    candidate_id,
92                    &summary_json,
93                    cea_fingerprint,
94                )?;
95                Ok(ArchiveUpdate::Replaced)
96            } else {
97                Ok(ArchiveUpdate::NoChange)
98            }
99        }
100        None => {
101            let summary_json = serde_json::to_string(&score_summary)?;
102            store.upsert_archive_cell(&cell_key, candidate_id, &summary_json, cea_fingerprint)?;
103            Ok(ArchiveUpdate::Inserted)
104        }
105    }
106}