pub mod analysis;
pub mod artifact;
pub mod ast;
pub mod canonical;
pub mod dedup;
pub mod dialect;
pub mod engine;
pub mod feedback;
pub mod measured_search;
pub mod novelty;
#[cfg(feature = "numeric-evidence")]
pub mod numeric;
pub mod operators;
#[cfg(feature = "parquet-input")]
pub mod parquet_input;
pub mod parser;
pub mod transform;
pub mod tree;
pub use analysis::{ExpressionAnalysis, RejectionReason, analyze_expression};
pub use artifact::{
ArtifactError, CANDIDATE_SCHEMA, CHECKPOINT_SCHEMA, CandidateRecord, CheckpointDraft,
FeedbackProvenance, MANIFEST_SCHEMA, OperatorPolicyProvenance, PublicationPointer,
RunCheckpoint, RunManifest, StructuralScoreComponents, publication_pointer_path,
read_candidates, read_checkpoint, read_published_run, write_checkpoint_atomic,
write_jsonl_atomic, write_manifest_atomic, write_run_transactional,
};
pub use ast::{Expr, ExprKind};
pub use canonical::{canonical, fingerprint, semantic_canonical, semantic_fingerprint};
pub use dedup::{
DedupError, DedupResult, DuplicateRecord, RejectedRecord, deduplicate, deduplicate_with_catalog,
};
pub use dialect::{
DIALECT_SCHEMA, DialectArtifact, DialectError, DialectSpec, UnaryScalarLowering,
compile_dialect,
};
pub use engine::{
SearchError, SearchGuidance, SearchResult, SearchRunOptions, run_search,
run_search_with_catalog, run_search_with_options, run_search_with_options_and_catalog,
};
pub use feedback::{
FEEDBACK_DATASET_SCHEMA, FeedbackAuditReport, FeedbackConfig, FeedbackDataset, FeedbackError,
FeedbackRecord, GUIDED_CANDIDATE_SCHEMA, GuidanceEstimate, GuidedCandidate,
OPERATOR_POLICY_SCHEMA, OperatorPolicyConfig, OperatorPolicyReport, audit_feedback,
candidate_features, prioritize_candidates, reweight_operator_catalog,
write_guided_jsonl_atomic,
};
pub use novelty::{
StructuralDescriptor, common_ancestor_depth, describe, describe_with_catalog,
descriptor_distance,
};
#[cfg(feature = "numeric-evidence")]
pub use numeric::{
BrainProxyMetrics, ColumnarDataset, DailyEvidence, EvaluationArtifact, EvaluationConfig,
NumericColumn, NumericError, NumericEvaluator, SplitEvidence, evaluate_numeric,
evaluate_numeric_with_catalog,
};
pub use operators::{
ArgumentSpec, Arity, Catalog, FieldSpec, GroupSpec, KeywordConstraint, KeywordSpec,
OperatorSpec, ScalarDomain, ValueDomain, builtin_catalog,
};
#[cfg(feature = "parquet-input")]
pub use parquet_input::{
ParquetInputError, PreparedPanel, SharadarPanelConfig, prepare_sharadar_panel,
};
pub use parser::{ExpressionError, parse_expression, parse_expression_with_catalog};
pub use transform::{
CATALOG_SCAFFOLD_OPERATION, CatalogScaffoldPolicy, Limits, MOTIF_CATALOG_SCAFFOLD_OPERATION,
MutationClass, Provenance, SEEDED_CATALOG_SCAFFOLD_OPERATION, TransformKind,
TransformValidationError, crossover, crossover_with_catalog, crossover_with_paths,
generate_with_catalog, generate_with_catalog_scaffold_policy, is_catalog_scaffold_operation,
mutate, mutate_applicable_with_catalog, mutate_with_catalog, mutate_with_class,
mutate_with_class_and_catalog, sample_with_catalog, validate_transformed,
validate_transformed_with_catalog,
};
pub use tree::{
ExprPath, KindSet, PathEntry, PathError, PathSegment, allowed_kinds, enumerate_paths,
replace_subtree, subtree_at,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const SEARCH_SPEC_SCHEMA: u32 = 2;
pub const MAX_CANDIDATES: u64 = 10_000_000;
pub const MAX_THREADS: usize = 256;
pub const MAX_SHORTLIST: usize = 100_000;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SearchSpec {
pub schema: u32,
pub seed: u64,
pub threads: usize,
pub max_candidates: u64,
pub duration_seconds: u64,
#[serde(default)]
pub limits: Limits,
#[serde(default)]
pub scoring: StructuralScoreConfig,
#[serde(default = "default_shortlist")]
pub shortlist_size: usize,
#[serde(default)]
pub resources: ResourceLimits,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceLimits {
pub population_capacity: usize,
pub parent_pool_capacity: usize,
pub elite_archive_capacity: usize,
pub semantic_archive_capacity: usize,
pub descriptor_archive_capacity: usize,
pub finalization_reserve_milliseconds: u64,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
population_capacity: 512,
parent_pool_capacity: 128,
elite_archive_capacity: 4_096,
semantic_archive_capacity: 100_000,
descriptor_archive_capacity: 4_096,
finalization_reserve_milliseconds: 250,
}
}
}
impl ResourceLimits {
fn validate(&self, shortlist_size: usize) -> Result<(), String> {
if self.population_capacity == 0
|| self.parent_pool_capacity == 0
|| self.elite_archive_capacity < shortlist_size
|| self.semantic_archive_capacity == 0
|| self.descriptor_archive_capacity == 0
{
return Err(
"capacities must be positive and elite archive must fit the shortlist".to_owned(),
);
}
if self.parent_pool_capacity > self.population_capacity
|| self.population_capacity > 100_000
|| self.elite_archive_capacity > 1_000_000
|| self.semantic_archive_capacity > 10_000_000
|| self.descriptor_archive_capacity > 1_000_000
|| self.finalization_reserve_milliseconds > 60_000
{
return Err("resource capacities exceed supported bounds".to_owned());
}
Ok(())
}
}
const fn default_shortlist() -> usize {
100
}
impl SearchSpec {
pub fn validate(&self) -> Result<(), SpecError> {
if self.schema != SEARCH_SPEC_SCHEMA {
return Err(SpecError::UnsupportedSchema(self.schema));
}
if self.threads == 0 {
return Err(SpecError::ZeroThreads);
}
if self.threads > MAX_THREADS {
return Err(SpecError::TooManyThreads(self.threads));
}
if self.max_candidates == 0 {
return Err(SpecError::ZeroCandidates);
}
if self.max_candidates > MAX_CANDIDATES {
return Err(SpecError::TooManyCandidates(self.max_candidates));
}
if self.duration_seconds == 0 {
return Err(SpecError::ZeroDuration);
}
if self.shortlist_size == 0 {
return Err(SpecError::ZeroShortlist);
}
if self.shortlist_size > MAX_SHORTLIST {
return Err(SpecError::TooLargeShortlist(self.shortlist_size));
}
self.limits.validate().map_err(SpecError::InvalidLimits)?;
self.scoring.validate().map_err(SpecError::InvalidScoring)?;
self.resources
.validate(self.shortlist_size)
.map_err(SpecError::InvalidResources)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum SpecError {
#[error("unsupported search specification schema {0}")]
UnsupportedSchema(u32),
#[error("threads must be greater than zero")]
ZeroThreads,
#[error("threads must not exceed {MAX_THREADS}, got {0}")]
TooManyThreads(usize),
#[error("max_candidates must be greater than zero")]
ZeroCandidates,
#[error("max_candidates must not exceed {MAX_CANDIDATES}, got {0}")]
TooManyCandidates(u64),
#[error("duration_seconds must be greater than zero")]
ZeroDuration,
#[error("shortlist_size must be greater than zero")]
ZeroShortlist,
#[error("shortlist_size must not exceed {MAX_SHORTLIST}, got {0}")]
TooLargeShortlist(usize),
#[error("invalid expression limits: {0}")]
InvalidLimits(String),
#[error("invalid structural scoring configuration: {0}")]
InvalidScoring(String),
#[error("invalid resource limits: {0}")]
InvalidResources(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StructuralScoreConfig {
pub schema: u32,
pub simplicity_weight: f64,
pub archive_novelty_weight: f64,
pub family_diversity_weight: f64,
pub lineage_diversity_weight: f64,
#[serde(default = "default_structural_novelty_weight")]
pub structural_novelty_weight: f64,
#[serde(default = "default_transform_diversity_weight")]
pub transform_diversity_weight: f64,
}
const fn default_structural_novelty_weight() -> f64 {
0.35
}
const fn default_transform_diversity_weight() -> f64 {
0.10
}
impl Default for StructuralScoreConfig {
fn default() -> Self {
Self {
schema: 1,
simplicity_weight: 0.20,
archive_novelty_weight: 0.15,
family_diversity_weight: 0.10,
lineage_diversity_weight: 0.10,
structural_novelty_weight: default_structural_novelty_weight(),
transform_diversity_weight: default_transform_diversity_weight(),
}
}
}
impl StructuralScoreConfig {
fn validate(&self) -> Result<(), String> {
if self.schema != 1 {
return Err(format!("unsupported schema {}", self.schema));
}
let weights = [
self.simplicity_weight,
self.archive_novelty_weight,
self.family_diversity_weight,
self.lineage_diversity_weight,
self.structural_novelty_weight,
self.transform_diversity_weight,
];
if weights
.iter()
.any(|weight| !weight.is_finite() || *weight < 0.0)
{
return Err("weights must be finite and non-negative".to_owned());
}
if weights.iter().sum::<f64>() == 0.0 {
return Err("at least one weight must be positive".to_owned());
}
Ok(())
}
#[must_use]
pub fn normalized(&self) -> StructuralScoreWeights {
let total = self.simplicity_weight
+ self.archive_novelty_weight
+ self.family_diversity_weight
+ self.lineage_diversity_weight
+ self.structural_novelty_weight
+ self.transform_diversity_weight;
StructuralScoreWeights {
simplicity: self.simplicity_weight / total,
archive_novelty: self.archive_novelty_weight / total,
family_diversity: self.family_diversity_weight / total,
lineage_diversity: self.lineage_diversity_weight / total,
structural_novelty: self.structural_novelty_weight / total,
transform_diversity: self.transform_diversity_weight / total,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct StructuralScoreWeights {
pub simplicity: f64,
pub archive_novelty: f64,
pub family_diversity: f64,
pub lineage_diversity: f64,
pub structural_novelty: f64,
pub transform_diversity: f64,
}
impl Default for StructuralScoreWeights {
fn default() -> Self {
StructuralScoreConfig::default().normalized()
}
}
#[cfg(test)]
mod tests {
use super::{ResourceLimits, SEARCH_SPEC_SCHEMA, SearchSpec, SpecError, StructuralScoreConfig};
use crate::Limits;
#[test]
fn valid_spec_round_trips_as_json() {
let spec = SearchSpec {
schema: SEARCH_SPEC_SCHEMA,
seed: 42,
threads: 4,
max_candidates: 100_000,
duration_seconds: 3_600,
limits: Limits::default(),
scoring: StructuralScoreConfig::default(),
shortlist_size: 100,
resources: ResourceLimits::default(),
};
spec.validate().expect("valid search spec");
let encoded = serde_json::to_string(&spec).expect("serialize search spec");
let decoded: SearchSpec = serde_json::from_str(&encoded).expect("parse search spec");
assert_eq!(decoded, spec);
}
#[test]
fn zero_threads_are_rejected() {
let spec = SearchSpec {
schema: SEARCH_SPEC_SCHEMA,
seed: 42,
threads: 0,
max_candidates: 1,
duration_seconds: 1,
limits: Limits::default(),
scoring: StructuralScoreConfig::default(),
shortlist_size: 100,
resources: ResourceLimits::default(),
};
assert_eq!(spec.validate(), Err(SpecError::ZeroThreads));
}
}