Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,657 @@
//! TimePuzzles Generator
//!
//! Generates constraint-based temporal reasoning puzzles
//! based on the TimePuzzles benchmark methodology (arXiv:2601.07148)
//!
//! Key features:
//! - Factual temporal anchors with calendar relations
//! - Cross-cultural date systems
//! - Controlled difficulty levels
//! - Dynamic puzzle generation
use crate::temporal::{TemporalConstraint, TemporalPuzzle};
use anyhow::Result;
use chrono::{Datelike, NaiveDate};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
/// Multi-dimensional difficulty vector.
///
/// Replaces single-axis difficulty to prevent collapsing effects.
/// Higher difficulty = more work and more ambiguity, NOT tighter posterior.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DifficultyVector {
/// Size of the search range (days)
pub range_size: usize,
/// Target number of valid candidates in posterior
pub posterior_target: usize,
/// Rate of distractor constraints (0.0 - 1.0)
pub distractor_rate: f64,
/// Rate of noise injection (0.0 - 1.0)
pub noise_rate: f64,
/// Number of ambiguous solutions (dates that almost satisfy constraints)
pub ambiguity_count: usize,
}
impl Default for DifficultyVector {
fn default() -> Self {
Self {
range_size: 60,
posterior_target: 60,
distractor_rate: 0.0,
noise_rate: 0.0,
ambiguity_count: 0,
}
}
}
impl DifficultyVector {
/// Build from scalar difficulty (backward compatible).
/// Higher difficulty = wider range, more distractors, more ambiguity.
pub fn from_scalar(difficulty: u8) -> Self {
let d = difficulty.min(10).max(1);
Self {
range_size: difficulty_to_range_size(d),
posterior_target: difficulty_to_posterior(d),
distractor_rate: difficulty_to_distractor_rate(d),
noise_rate: difficulty_to_noise_rate(d),
ambiguity_count: difficulty_to_ambiguity(d),
}
}
/// Scalar difficulty estimate (for backward compat).
pub fn scalar(&self) -> u8 {
// Weighted combination back to 1-10 scale
let range_score = (self.range_size as f64 / 365.0 * 10.0).min(10.0);
let distractor_score = self.distractor_rate * 10.0;
let ambiguity_score = (self.ambiguity_count as f64 / 5.0 * 10.0).min(10.0);
let combined = (range_score * 0.3 + distractor_score * 0.3 + ambiguity_score * 0.4) as u8;
combined.max(1).min(10)
}
}
/// Puzzle generator configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PuzzleGeneratorConfig {
/// Minimum difficulty (1-10)
pub min_difficulty: u8,
/// Maximum difficulty (1-10)
pub max_difficulty: u8,
/// Constraint density (1-5)
pub constraint_density: u8,
/// Include cross-cultural references
pub cross_cultural: bool,
/// Include relative constraints
pub relative_constraints: bool,
/// Year range for puzzles
pub year_range: (i32, i32),
/// Random seed (optional)
pub seed: Option<u64>,
}
impl Default for PuzzleGeneratorConfig {
fn default() -> Self {
Self {
min_difficulty: 1,
max_difficulty: 10,
constraint_density: 3,
cross_cultural: true,
relative_constraints: true,
year_range: (2000, 2030),
seed: None,
}
}
}
/// Known events for temporal anchoring
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TemporalAnchor {
pub name: String,
pub date: NaiveDate,
pub category: String,
pub culture: String,
}
impl TemporalAnchor {
pub fn new(
name: impl Into<String>,
year: i32,
month: u32,
day: u32,
category: impl Into<String>,
culture: impl Into<String>,
) -> Self {
Self {
name: name.into(),
date: NaiveDate::from_ymd_opt(year, month, day).unwrap(),
category: category.into(),
culture: culture.into(),
}
}
}
/// TimePuzzles generator
pub struct PuzzleGenerator {
config: PuzzleGeneratorConfig,
anchors: Vec<TemporalAnchor>,
rng: StdRng,
}
impl PuzzleGenerator {
/// Create a new generator with config
pub fn new(config: PuzzleGeneratorConfig) -> Self {
let rng = match config.seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::from_entropy(),
};
let mut gen = Self {
config,
anchors: Vec::new(),
rng,
};
gen.init_anchors();
gen
}
/// Initialize standard temporal anchors
fn init_anchors(&mut self) {
// Western holidays
self.anchors.push(TemporalAnchor::new(
"Christmas",
2024,
12,
25,
"holiday",
"western",
));
self.anchors.push(TemporalAnchor::new(
"New Year", 2024, 1, 1, "holiday", "western",
));
self.anchors.push(TemporalAnchor::new(
"Independence Day",
2024,
7,
4,
"holiday",
"american",
));
self.anchors.push(TemporalAnchor::new(
"Halloween",
2024,
10,
31,
"holiday",
"western",
));
self.anchors.push(TemporalAnchor::new(
"Valentine's Day",
2024,
2,
14,
"holiday",
"western",
));
// Cross-cultural events
if self.config.cross_cultural {
// Chinese New Year 2024 (Year of the Dragon)
self.anchors.push(TemporalAnchor::new(
"Chinese New Year 2024",
2024,
2,
10,
"holiday",
"chinese",
));
// Diwali 2024
self.anchors.push(TemporalAnchor::new(
"Diwali 2024",
2024,
11,
1,
"holiday",
"indian",
));
// Eid al-Fitr 2024
self.anchors.push(TemporalAnchor::new(
"Eid al-Fitr 2024",
2024,
4,
10,
"holiday",
"islamic",
));
// Hanukkah 2024 (starts)
self.anchors.push(TemporalAnchor::new(
"Hanukkah 2024",
2024,
12,
25,
"holiday",
"jewish",
));
}
// Historical events
self.anchors.push(TemporalAnchor::new(
"Moon Landing",
1969,
7,
20,
"historical",
"global",
));
self.anchors.push(TemporalAnchor::new(
"Fall of Berlin Wall",
1989,
11,
9,
"historical",
"global",
));
self.anchors.push(TemporalAnchor::new(
"Y2K",
2000,
1,
1,
"historical",
"global",
));
}
/// Generate a single puzzle with multi-dimensional difficulty vector.
///
/// Difficulty scaling (higher = more work, not tighter posterior):
/// - Low (1-2): small range, no DayOfWeek, no distractors
/// - Medium (3-6): DayOfWeek + moderate range = 7x cost surface
/// - High (7-10): wide range + distractors + ambiguity + anchor constraints
///
/// All modes have access to weekday skipping; what differs is the policy.
pub fn generate_puzzle(&mut self, id: impl Into<String>) -> Result<TemporalPuzzle> {
let id = id.into();
let difficulty = self
.rng
.gen_range(self.config.min_difficulty..=self.config.max_difficulty);
// Build difficulty vector from scalar
let dv = DifficultyVector::from_scalar(difficulty);
// DayOfWeek (difficulty 3+): creates cost surface for policy decisions
let use_day_of_week = difficulty >= 3;
// Range size from difficulty vector (wider range at higher difficulty)
let range_days = dv.range_size as i64;
// Pick target date
let year = self
.rng
.gen_range(self.config.year_range.0..=self.config.year_range.1);
let month = self.rng.gen_range(1..=12);
let max_day = days_in_month(year, month);
let day = self.rng.gen_range(1..=max_day);
let target = NaiveDate::from_ymd_opt(year, month, day).unwrap();
// Build Between range centered on target, clamped to year
let year_start = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
let year_end = NaiveDate::from_ymd_opt(year, 12, 31).unwrap();
let half = range_days / 2;
let range_start = (target - chrono::Duration::days(half)).max(year_start);
let range_end = (range_start + chrono::Duration::days(range_days - 1)).min(year_end);
let mut puzzle = TemporalPuzzle::new(id.clone(), format!("Find the date (puzzle {})", id))
.with_difficulty(difficulty)
.with_solutions(vec![target]);
// Attach difficulty vector
puzzle.difficulty_vector = Some(dv.clone());
// Base constraints: InYear + Between (defines search range)
puzzle
.constraints
.push(TemporalConstraint::InYear(target.year()));
puzzle
.constraints
.push(TemporalConstraint::Between(range_start, range_end));
let mut used_anchors: Vec<TemporalAnchor> = Vec::new();
// DayOfWeek (difficulty 3+): creates cost surface for all modes
if use_day_of_week {
puzzle
.constraints
.push(TemporalConstraint::DayOfWeek(target.weekday()));
}
// Anchor reference for high difficulty (7+)
if difficulty >= 7 && self.config.relative_constraints {
if let Some(anchor) = self.anchors.choose(&mut self.rng).cloned() {
let diff = (target - anchor.date).num_days();
let constraint = if diff >= 0 {
TemporalConstraint::DaysAfter(anchor.name.clone(), diff)
} else {
TemporalConstraint::DaysBefore(anchor.name.clone(), diff.abs())
};
puzzle.constraints.push(constraint);
used_anchors.push(anchor);
}
}
// Add anchor references
for anchor in used_anchors {
puzzle.references.insert(anchor.name.clone(), anchor.date);
}
// Distractor injection (from difficulty vector rate)
if dv.distractor_rate > 0.0 && self.rng.gen_bool(dv.distractor_rate.min(0.99)) {
let distractor = self.generate_distractor(target, range_start, range_end);
puzzle.constraints.push(distractor);
}
// Distractor DayOfWeek (difficulty 6+): DayOfWeek present but misleading.
// Adds a SECOND DayOfWeek that is a distractor — it matches the target
// but unconditional weekday skipping on the wrong dow will miss solutions.
// This creates a real tradeoff for the PolicyKernel.
if difficulty >= 6 && use_day_of_week {
let distractor_dow_chance: f64 = match difficulty {
6 => 0.15,
7 => 0.25,
8 => 0.35,
9..=10 => 0.50,
_ => 0.0,
};
if self.rng.gen_bool(distractor_dow_chance.min(0.99)) {
// Add a redundant wider Between that doesn't narrow search
// but pairs with the existing DayOfWeek to create a trap:
// the DayOfWeek is valid but the wider range means skip saves less
let wider_start = range_start - chrono::Duration::days(self.rng.gen_range(14..60));
let wider_end = range_end + chrono::Duration::days(self.rng.gen_range(14..60));
puzzle
.constraints
.push(TemporalConstraint::Between(wider_start, wider_end));
}
}
// Ambiguity: add near-miss solutions at high difficulty
// These are dates that satisfy most but not all constraints,
// making early commits risky.
if dv.ambiguity_count > 0 {
// No-op structurally (solutions list stays correct),
// but the wider range at high difficulty naturally creates more
// dates that pass most constraints, increasing false-positive risk
// for aggressive skip modes.
}
// Count actual distractors injected (deterministic, observable)
let actual_distractor_count = crate::temporal::count_distractors(&puzzle);
// Tags: all features visible to policies for deterministic observability
puzzle.tags = vec![
format!("difficulty:{}", difficulty),
format!("year:{}", year),
format!("range_size:{}", dv.range_size),
format!("distractor_rate:{:.2}", dv.distractor_rate),
format!("distractor_count:{}", actual_distractor_count),
format!("ambiguity:{}", dv.ambiguity_count),
format!("has_dow:{}", use_day_of_week),
];
Ok(puzzle)
}
/// Generate a distractor constraint: true for the target but doesn't narrow the search.
fn generate_distractor(
&mut self,
target: NaiveDate,
range_start: NaiveDate,
range_end: NaiveDate,
) -> TemporalConstraint {
match self.rng.gen_range(0u8..3) {
0 => {
// Wider Between (superset of existing range → no shrink)
let wider_start = range_start - chrono::Duration::days(self.rng.gen_range(10..60));
let wider_end = range_end + chrono::Duration::days(self.rng.gen_range(10..60));
TemporalConstraint::Between(wider_start, wider_end)
}
1 => {
// Redundant InYear (already present)
TemporalConstraint::InYear(target.year())
}
_ => {
// After a date well before the range (no shrink)
let days_before = self.rng.gen_range(30..180) as i64;
TemporalConstraint::After(target - chrono::Duration::days(days_before))
}
}
}
/// Generate a batch of puzzles
pub fn generate_batch(&mut self, count: usize) -> Result<Vec<TemporalPuzzle>> {
let mut puzzles = Vec::with_capacity(count);
for i in 0..count {
let puzzle = self.generate_puzzle(format!("puzzle-{:04}", i + 1))?;
puzzles.push(puzzle);
}
Ok(puzzles)
}
/// Generate puzzles at specific difficulty
pub fn generate_at_difficulty(
&mut self,
count: usize,
difficulty: u8,
) -> Result<Vec<TemporalPuzzle>> {
let orig_min = self.config.min_difficulty;
let orig_max = self.config.max_difficulty;
self.config.min_difficulty = difficulty;
self.config.max_difficulty = difficulty;
let puzzles = self.generate_batch(count);
self.config.min_difficulty = orig_min;
self.config.max_difficulty = orig_max;
puzzles
}
}
/// Range size by difficulty level.
/// Higher difficulty → wider range → more work for the solver.
fn difficulty_to_range_size(difficulty: u8) -> usize {
match difficulty {
1 => 14,
2 => 30,
3 => 56, // 8 weeks
4 => 84, // 12 weeks
5 => 120,
6 => 150,
7 => 200,
8 => 250,
9 => 300,
10 => 365,
_ => 120,
}
}
/// Posterior target by difficulty level.
/// Higher difficulty → more valid candidates → more ambiguity.
/// (Flipped from old model: difficulty increases ambiguity, not reduces it.)
fn difficulty_to_posterior(difficulty: u8) -> usize {
match difficulty {
1 => 2,
2 => 4,
3 => 8,
4 => 12,
5 => 18,
6 => 25,
7 => 35,
8 => 50,
9 => 70,
10 => 100,
_ => 18,
}
}
/// Distractor rate by difficulty level.
fn difficulty_to_distractor_rate(difficulty: u8) -> f64 {
match difficulty {
1..=3 => 0.0,
4 => 0.05,
5 => 0.10,
6 => 0.20,
7 => 0.30,
8 => 0.40,
9 => 0.50,
10 => 0.60,
_ => 0.10,
}
}
/// Noise rate by difficulty level.
fn difficulty_to_noise_rate(difficulty: u8) -> f64 {
match difficulty {
1..=3 => 0.0,
4..=5 => 0.10,
6..=7 => 0.20,
8..=9 => 0.30,
10 => 0.40,
_ => 0.10,
}
}
/// Ambiguity count by difficulty level (near-miss solutions).
fn difficulty_to_ambiguity(difficulty: u8) -> usize {
match difficulty {
1..=4 => 0,
5..=6 => 1,
7..=8 => 2,
9 => 3,
10 => 5,
_ => 0,
}
}
/// Days in a given month (handles leap years).
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
4 | 6 | 9 | 11 => 30,
2 => {
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
29
} else {
28
}
}
_ => 31,
}
}
/// Sample puzzle sets
pub struct SamplePuzzles;
impl SamplePuzzles {
/// Get easy puzzles (difficulty 1-3)
pub fn easy() -> Vec<TemporalPuzzle> {
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
min_difficulty: 1,
max_difficulty: 3,
seed: Some(42),
..Default::default()
});
gen.generate_batch(10).unwrap()
}
/// Get medium puzzles (difficulty 4-6)
pub fn medium() -> Vec<TemporalPuzzle> {
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
min_difficulty: 4,
max_difficulty: 6,
seed: Some(42),
..Default::default()
});
gen.generate_batch(10).unwrap()
}
/// Get hard puzzles (difficulty 7-10)
pub fn hard() -> Vec<TemporalPuzzle> {
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
min_difficulty: 7,
max_difficulty: 10,
seed: Some(42),
..Default::default()
});
gen.generate_batch(10).unwrap()
}
/// Get cross-cultural puzzles
pub fn cross_cultural() -> Vec<TemporalPuzzle> {
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
cross_cultural: true,
relative_constraints: true,
min_difficulty: 5,
max_difficulty: 8,
seed: Some(42),
..Default::default()
});
gen.generate_batch(10).unwrap()
}
/// Get a mixed sample set (50 puzzles across all difficulties)
pub fn mixed_sample() -> Vec<TemporalPuzzle> {
let mut all = Vec::new();
all.extend(Self::easy());
all.extend(Self::medium());
all.extend(Self::hard());
all.extend(Self::cross_cultural());
// Add more easy/medium to match TimePuzzles distribution
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
min_difficulty: 2,
max_difficulty: 5,
seed: Some(123),
..Default::default()
});
all.extend(gen.generate_batch(10).unwrap());
all
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_puzzle_generation() {
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
seed: Some(42),
..Default::default()
});
let puzzle = gen.generate_puzzle("test-1").unwrap();
assert!(!puzzle.constraints.is_empty());
assert!(!puzzle.solutions.is_empty());
}
#[test]
fn test_batch_generation() {
let mut gen = PuzzleGenerator::new(PuzzleGeneratorConfig {
seed: Some(42),
..Default::default()
});
let puzzles = gen.generate_batch(20).unwrap();
assert_eq!(puzzles.len(), 20);
}
#[test]
fn test_sample_puzzles() {
let easy = SamplePuzzles::easy();
assert_eq!(easy.len(), 10);
assert!(easy.iter().all(|p| p.difficulty <= 3));
let hard = SamplePuzzles::hard();
assert!(hard.iter().all(|p| p.difficulty >= 7));
}
}