1use forge_engine::{ExperimentConfig, ForgeConfig, StructuredPatch};
7use knowledge_runtime::{RuntimeConfig, Scope};
8use verification_policy::{ApprovalRecord, PolicySnapshot};
9
10#[derive(Debug, Clone)]
12pub struct PatchPlanSeed {
13 pub target_key: String,
14 pub fixture_path: String,
15 pub patch: StructuredPatch,
16 pub experiment_config: ExperimentConfig,
17 pub description: String,
18}
19
20#[derive(Debug, Clone)]
25pub struct LoopConfig {
26 pub max_iterations: u32,
27 pub time_budget_secs: u64,
28 pub cooldown_secs: u64,
29 pub halt_urgency_threshold: f64,
30 pub max_retries_per_target: u32,
31 pub allow_advisory_only_steps: bool,
32 pub scope: Scope,
33 pub workspace_path: String,
34 pub memory_dir: String,
35 pub forge_db_path: String,
36 pub include_hyperedges: bool,
37 pub oracle_max_iterations: u32,
38 pub minimal_perturbation_budget: usize,
39 pub observation_import_limit: usize,
40 pub observation_projection_limit: usize,
41 pub runtime_config: RuntimeConfig,
42 pub forge_config: ForgeConfig,
43 pub patch_plan_seeds: Vec<PatchPlanSeed>,
44 pub policy_snapshots: Vec<PolicySnapshot>,
45 pub approval_records: Vec<ApprovalRecord>,
46 pub calibration_abstention_threshold_micros: u64,
47}
48
49impl LoopConfig {
50 pub fn default_for_scope(scope: Scope, workspace_path: impl Into<String>) -> Self {
52 Self {
53 scope: scope.clone(),
54 workspace_path: workspace_path.into(),
55 runtime_config: RuntimeConfig {
56 default_scope: scope,
57 ..RuntimeConfig {
58 default_scope: Scope::new("default"),
59 query: Default::default(),
60 entity: Default::default(),
61 projection: Default::default(),
62 strict_temporal: false,
63 strict_scope: false,
64 }
65 },
66 ..Self::default()
67 }
68 }
69
70 pub fn patch_seed_for(&self, target_key: &str) -> Option<&PatchPlanSeed> {
72 self.patch_plan_seeds
73 .iter()
74 .find(|seed| seed.target_key == target_key)
75 .or_else(|| {
76 let target_family = patch_seed_target_family(target_key)?;
77 self.patch_plan_seeds.iter().find(|seed| {
78 patch_seed_target_family(seed.target_key.as_str()) == Some(target_family)
79 })
80 })
81 }
82}
83
84fn patch_seed_target_family(target_key: &str) -> Option<&str> {
85 target_key.split_once(':').map(|(family, _)| family)
86}
87
88impl Default for LoopConfig {
89 fn default() -> Self {
90 let default_scope = Scope::new("default");
91 Self {
92 max_iterations: 4,
93 time_budget_secs: 30,
94 cooldown_secs: 0,
95 halt_urgency_threshold: 0.25,
96 max_retries_per_target: 2,
97 allow_advisory_only_steps: false,
98 scope: default_scope.clone(),
99 workspace_path: ".".into(),
100 memory_dir: "./memory".into(),
101 forge_db_path: "./forge.db".into(),
102 include_hyperedges: true,
103 oracle_max_iterations: 4,
104 minimal_perturbation_budget: 2,
105 observation_import_limit: 16,
106 observation_projection_limit: 128,
107 runtime_config: RuntimeConfig {
108 default_scope,
109 query: Default::default(),
110 entity: Default::default(),
111 projection: Default::default(),
112 strict_temporal: false,
113 strict_scope: false,
114 },
115 forge_config: ForgeConfig::default(),
116 patch_plan_seeds: Vec::new(),
117 policy_snapshots: vec![PolicySnapshot::permissive(
118 "forge-pilot.default",
119 "2026-03-12T00:00:00Z",
120 )],
121 approval_records: Vec::new(),
122 calibration_abstention_threshold_micros: 500_000,
123 }
124 }
125}