1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::time::SystemTime;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ModelTier {
13 Architect,
15 Actuator,
17 Verifier,
19 Speculator,
21}
22
23impl ModelTier {
24 pub fn default_model(&self) -> &'static str {
31 match self {
32 ModelTier::Architect => "gemini-3.1-pro-preview",
33 ModelTier::Verifier => "gemini-3.1-pro-preview",
34 ModelTier::Actuator => "gemini-3.1-flash-lite-preview",
35 ModelTier::Speculator => "gemini-3.1-flash-lite-preview",
36 }
37 }
38
39 pub fn default_model_name() -> &'static str {
42 "gemini-3.1-flash-lite-preview"
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::ModelTier;
49
50 #[test]
51 fn gemini_defaults_use_requested_latest_models() {
52 assert_eq!(
53 ModelTier::Architect.default_model(),
54 "gemini-3.1-pro-preview"
55 );
56 assert_eq!(
57 ModelTier::Verifier.default_model(),
58 "gemini-3.1-pro-preview"
59 );
60 assert_eq!(
61 ModelTier::Actuator.default_model(),
62 "gemini-3.1-flash-lite-preview"
63 );
64 assert_eq!(
65 ModelTier::Speculator.default_model(),
66 "gemini-3.1-flash-lite-preview"
67 );
68 assert_eq!(
69 ModelTier::default_model_name(),
70 "gemini-3.1-flash-lite-preview"
71 );
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub enum Criticality {
78 Critical,
80 High,
82 Low,
84}
85
86impl Criticality {
87 pub fn weight(&self) -> f32 {
89 match self {
90 Criticality::Critical => 10.0,
91 Criticality::High => 3.0,
92 Criticality::Low => 1.0,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct WeightedTest {
100 pub test_name: String,
102 pub criticality: Criticality,
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
110pub struct BehavioralContract {
111 pub interface_signature: String,
113 pub invariants: Vec<String>,
115 pub forbidden_patterns: Vec<String>,
117 pub weighted_tests: Vec<WeightedTest>,
119 pub energy_weights: (f32, f32, f32),
122}
123
124impl BehavioralContract {
125 pub fn new() -> Self {
127 Self {
128 interface_signature: String::new(),
129 invariants: Vec::new(),
130 forbidden_patterns: Vec::new(),
131 weighted_tests: Vec::new(),
132 energy_weights: (1.0, 0.5, 2.0), }
134 }
135
136 pub fn alpha(&self) -> f32 {
138 self.energy_weights.0
139 }
140
141 pub fn beta(&self) -> f32 {
143 self.energy_weights.1
144 }
145
146 pub fn gamma(&self) -> f32 {
148 self.energy_weights.2
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
154pub enum ErrorType {
155 #[default]
157 Compilation,
158 ToolFailure,
160 ReviewRejection,
162 Other,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct RetryPolicy {
169 pub max_compilation_retries: usize,
171 pub max_tool_retries: usize,
173 pub max_review_rejections: usize,
175 pub compilation_failures: usize,
177 pub tool_failures: usize,
178 pub review_rejections: usize,
179 pub last_error_type: Option<ErrorType>,
181}
182
183impl Default for RetryPolicy {
184 fn default() -> Self {
185 Self {
186 max_compilation_retries: 3,
188 max_tool_retries: 5,
189 max_review_rejections: 3,
190 compilation_failures: 0,
191 tool_failures: 0,
192 review_rejections: 0,
193 last_error_type: None,
194 }
195 }
196}
197
198impl RetryPolicy {
199 pub fn record_failure(&mut self, error_type: ErrorType) {
201 self.last_error_type = Some(error_type);
202 match error_type {
203 ErrorType::Compilation => self.compilation_failures += 1,
204 ErrorType::ToolFailure => self.tool_failures += 1,
205 ErrorType::ReviewRejection => self.review_rejections += 1,
206 ErrorType::Other => self.compilation_failures += 1, }
208 }
209
210 pub fn should_escalate(&self, error_type: ErrorType) -> bool {
212 match error_type {
213 ErrorType::Compilation | ErrorType::Other => {
214 self.compilation_failures >= self.max_compilation_retries
215 }
216 ErrorType::ToolFailure => self.tool_failures >= self.max_tool_retries,
217 ErrorType::ReviewRejection => self.review_rejections >= self.max_review_rejections,
218 }
219 }
220
221 pub fn any_exceeded(&self) -> bool {
223 self.compilation_failures >= self.max_compilation_retries
224 || self.tool_failures >= self.max_tool_retries
225 || self.review_rejections >= self.max_review_rejections
226 }
227
228 pub fn remaining_attempts(&self, error_type: ErrorType) -> usize {
230 match error_type {
231 ErrorType::Compilation | ErrorType::Other => self
232 .max_compilation_retries
233 .saturating_sub(self.compilation_failures),
234 ErrorType::ToolFailure => self.max_tool_retries.saturating_sub(self.tool_failures),
235 ErrorType::ReviewRejection => self
236 .max_review_rejections
237 .saturating_sub(self.review_rejections),
238 }
239 }
240
241 pub fn summary(&self) -> String {
243 format!(
244 "Retries: comp {}/{}, tool {}/{}, review {}/{}",
245 self.compilation_failures,
246 self.max_compilation_retries,
247 self.tool_failures,
248 self.max_tool_retries,
249 self.review_rejections,
250 self.max_review_rejections
251 )
252 }
253}
254
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
257pub struct StabilityMonitor {
258 pub energy_history: Vec<f32>,
260 pub attempt_count: usize,
262 pub stable: bool,
264 pub stability_epsilon: f32,
266 pub max_retries: usize,
268 pub retry_policy: RetryPolicy,
270}
271
272impl StabilityMonitor {
273 pub fn new() -> Self {
275 Self {
276 energy_history: Vec::new(),
277 attempt_count: 0,
278 stable: false,
279 stability_epsilon: 0.1,
280 max_retries: 3,
281 retry_policy: RetryPolicy::default(),
282 }
283 }
284
285 pub fn record_energy(&mut self, energy: f32) {
287 self.energy_history.push(energy);
288 self.attempt_count += 1;
289 self.stable = energy < self.stability_epsilon;
290 }
291
292 pub fn record_failure(&mut self, error_type: ErrorType) {
294 self.retry_policy.record_failure(error_type);
295 }
296
297 pub fn should_escalate(&self) -> bool {
299 (self.attempt_count >= self.max_retries && !self.stable) || self.retry_policy.any_exceeded()
301 }
302
303 pub fn remaining_attempts(&self) -> usize {
305 match self.retry_policy.last_error_type {
306 Some(et) => self.retry_policy.remaining_attempts(et),
307 None => self.max_retries.saturating_sub(self.attempt_count),
308 }
309 }
310
311 pub fn current_energy(&self) -> f32 {
313 self.energy_history.last().copied().unwrap_or(f32::INFINITY)
314 }
315
316 pub fn is_converging(&self) -> bool {
318 if self.energy_history.len() < 2 {
319 return true; }
321 let last = self.energy_history.last().unwrap();
322 let prev = &self.energy_history[self.energy_history.len() - 2];
323 last < prev
324 }
325
326 pub fn reset_for_replan(&mut self) {
329 self.attempt_count = 0;
330 self.stable = false;
331 self.retry_policy = RetryPolicy::default();
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct SRBNNode {
338 pub node_id: String,
340 pub goal: String,
342 pub context_files: Vec<PathBuf>,
344 pub output_targets: Vec<PathBuf>,
346 pub contract: BehavioralContract,
348 pub tier: ModelTier,
350 pub monitor: StabilityMonitor,
352 pub state: NodeState,
354 pub parent_id: Option<String>,
356 pub children: Vec<String>,
358 pub node_class: NodeClass,
360 pub owner_plugin: String,
362 pub provisional_branch_id: Option<String>,
364 pub interface_seal_hash: Option<[u8; 32]>,
366 pub dependency_expectations: DependencyExpectation,
368}
369
370impl SRBNNode {
371 pub fn new(node_id: String, goal: String, tier: ModelTier) -> Self {
373 Self {
374 node_id,
375 goal,
376 context_files: Vec::new(),
377 output_targets: Vec::new(),
378 contract: BehavioralContract::new(),
379 tier,
380 monitor: StabilityMonitor::new(),
381 state: NodeState::TaskQueued,
382 parent_id: None,
383 children: Vec::new(),
384 node_class: NodeClass::default(),
385 owner_plugin: String::new(),
386 provisional_branch_id: None,
387 interface_seal_hash: None,
388 dependency_expectations: DependencyExpectation::default(),
389 }
390 }
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395pub enum SessionOutcome {
396 Success,
398 PartialSuccess,
400 Failed,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
406pub enum NodeState {
407 TaskQueued,
409 Planning,
411 Coding,
413 Verifying,
415 Retry,
417 SheafCheck,
419 Committing,
421 Escalated,
423 Completed,
425 Failed,
427 Aborted,
429 Superseded,
431}
432
433impl NodeState {
434 pub fn is_terminal(&self) -> bool {
436 matches!(
437 self,
438 NodeState::Completed | NodeState::Failed | NodeState::Aborted | NodeState::Superseded
439 )
440 }
441
442 pub fn is_success(&self) -> bool {
444 matches!(self, NodeState::Completed)
445 }
446
447 pub fn is_active(&self) -> bool {
449 matches!(
450 self,
451 NodeState::Planning
452 | NodeState::Coding
453 | NodeState::Verifying
454 | NodeState::Retry
455 | NodeState::SheafCheck
456 | NodeState::Committing
457 )
458 }
459
460 pub fn from_display_str(s: &str) -> Self {
465 match s.to_ascii_lowercase().as_str() {
466 "taskqueued" | "queued" | "task_queued" => NodeState::TaskQueued,
467 "planning" => NodeState::Planning,
468 "coding" | "in_progress" | "in-progress" | "running" => NodeState::Coding,
469 "verifying" => NodeState::Verifying,
470 "retry" | "retrying" => NodeState::Retry,
471 "sheafcheck" | "sheaf_check" => NodeState::SheafCheck,
472 "committing" | "committed" => NodeState::Committing,
473 "escalated" => NodeState::Escalated,
474 "completed" | "stable" | "verified" => NodeState::Completed,
475 "failed" | "error" => NodeState::Failed,
476 "aborted" => NodeState::Aborted,
477 "superseded" => NodeState::Superseded,
478 _ => NodeState::TaskQueued,
479 }
480 }
481}
482
483impl std::fmt::Display for NodeState {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485 let label = match self {
486 NodeState::TaskQueued => "queued",
487 NodeState::Planning => "planning",
488 NodeState::Coding => "coding",
489 NodeState::Verifying => "verifying",
490 NodeState::Retry => "retrying",
491 NodeState::SheafCheck => "sheaf_check",
492 NodeState::Committing => "committing",
493 NodeState::Escalated => "escalated",
494 NodeState::Completed => "completed",
495 NodeState::Failed => "failed",
496 NodeState::Aborted => "aborted",
497 NodeState::Superseded => "superseded",
498 };
499 f.write_str(label)
500 }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct TokenBudget {
508 pub max_tokens: usize,
510 pub max_cost_usd: Option<f64>,
512 pub input_tokens_used: usize,
514 pub output_tokens_used: usize,
516 pub cost_usd: f64,
518 pub input_cost_per_1k: f64,
520 pub output_cost_per_1k: f64,
522}
523
524impl Default for TokenBudget {
525 fn default() -> Self {
526 Self {
527 max_tokens: 100_000, max_cost_usd: None, input_tokens_used: 0,
530 output_tokens_used: 0,
531 cost_usd: 0.0,
532 input_cost_per_1k: 0.075 / 1000.0, output_cost_per_1k: 0.30 / 1000.0, }
536 }
537}
538
539impl TokenBudget {
540 pub fn new(max_tokens: usize, max_cost_usd: Option<f64>) -> Self {
542 Self {
543 max_tokens,
544 max_cost_usd,
545 ..Default::default()
546 }
547 }
548
549 pub fn record_usage(&mut self, input_tokens: usize, output_tokens: usize) {
551 self.input_tokens_used += input_tokens;
552 self.output_tokens_used += output_tokens;
553
554 let input_cost = (input_tokens as f64 / 1000.0) * self.input_cost_per_1k;
556 let output_cost = (output_tokens as f64 / 1000.0) * self.output_cost_per_1k;
557 self.cost_usd += input_cost + output_cost;
558 }
559
560 pub fn total_tokens_used(&self) -> usize {
562 self.input_tokens_used + self.output_tokens_used
563 }
564
565 pub fn remaining_tokens(&self) -> usize {
567 self.max_tokens.saturating_sub(self.total_tokens_used())
568 }
569
570 pub fn is_exhausted(&self) -> bool {
572 self.total_tokens_used() >= self.max_tokens
573 }
574
575 pub fn cost_exceeded(&self) -> bool {
577 if let Some(max_cost) = self.max_cost_usd {
578 self.cost_usd >= max_cost
579 } else {
580 false
581 }
582 }
583
584 pub fn should_stop(&self) -> bool {
586 self.is_exhausted() || self.cost_exceeded()
587 }
588
589 pub fn usage_percent(&self) -> f32 {
591 if self.max_tokens == 0 {
592 0.0
593 } else {
594 (self.total_tokens_used() as f32 / self.max_tokens as f32) * 100.0
595 }
596 }
597
598 pub fn set_pricing(&mut self, input_per_1k: f64, output_per_1k: f64) {
600 self.input_cost_per_1k = input_per_1k;
601 self.output_cost_per_1k = output_per_1k;
602 }
603
604 pub fn summary(&self) -> String {
606 format!(
607 "Tokens: {}/{} ({:.1}%), Cost: ${:.4}{}",
608 self.total_tokens_used(),
609 self.max_tokens,
610 self.usage_percent(),
611 self.cost_usd,
612 self.max_cost_usd
613 .map(|m| format!(" / ${:.2}", m))
614 .unwrap_or_default()
615 )
616 }
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct AgentContext {
622 pub working_dir: PathBuf,
624 pub history: Vec<AgentMessage>,
626 pub merkle_root: [u8; 32],
628 pub complexity_k: usize,
630 pub session_id: String,
632 pub auto_approve: bool,
634 pub defer_tests: bool,
636 pub log_llm: bool,
638 #[serde(skip)]
640 pub last_diagnostics: Vec<lsp_types::Diagnostic>,
641 pub token_budget: TokenBudget,
643 #[serde(skip)]
645 pub last_test_output: Option<String>,
646 #[serde(default)]
648 pub execution_mode: ExecutionMode,
649 #[serde(default)]
651 pub verifier_strictness: VerifierStrictness,
652 #[serde(default)]
654 pub active_plugins: Vec<String>,
655 #[serde(default)]
657 pub workspace_state: WorkspaceState,
658 #[serde(default)]
660 pub ownership_manifest: OwnershipManifest,
661}
662
663impl Default for AgentContext {
664 fn default() -> Self {
665 Self {
666 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
667 history: Vec::new(),
668 merkle_root: [0u8; 32],
669 complexity_k: 5, session_id: uuid::Uuid::new_v4().to_string(),
671 auto_approve: false,
672 defer_tests: false,
673 log_llm: false,
674 last_diagnostics: Vec::new(),
675 token_budget: TokenBudget::default(),
676 last_test_output: None,
677 execution_mode: ExecutionMode::default(),
678 verifier_strictness: VerifierStrictness::default(),
679 active_plugins: Vec::new(),
680 workspace_state: WorkspaceState::default(),
681 ownership_manifest: OwnershipManifest::default(),
682 }
683 }
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct AgentMessage {
689 pub role: ModelTier,
691 pub content: String,
693 pub timestamp: SystemTime,
695 pub node_id: Option<String>,
697}
698
699impl AgentMessage {
700 pub fn new(role: ModelTier, content: String) -> Self {
702 Self {
703 role,
704 content,
705 timestamp: SystemTime::now(),
706 node_id: None,
707 }
708 }
709}
710
711#[derive(Debug, Clone, Default, Serialize, Deserialize)]
713pub struct EnergyComponents {
714 pub v_syn: f32,
716 pub v_str: f32,
718 pub v_log: f32,
720 pub v_boot: f32,
722 pub v_sheaf: f32,
724}
725
726impl EnergyComponents {
727 pub fn total(&self, contract: &BehavioralContract) -> f32 {
729 contract.alpha() * self.v_syn
730 + contract.beta() * self.v_str
731 + contract.gamma() * self.v_log
732 + self.v_boot
733 + self.v_sheaf
734 }
735
736 pub fn total_simple(&self) -> f32 {
739 self.v_syn + self.v_str + self.v_log + self.v_boot + self.v_sheaf
740 }
741}
742
743#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
749#[serde(rename_all = "snake_case")]
750pub enum TaskType {
751 #[default]
753 Code,
754 Command,
756 UnitTest,
758 IntegrationTest,
760 Refactor,
762 Documentation,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
769pub struct TaskPlan {
770 pub tasks: Vec<PlannedTask>,
772}
773
774impl TaskPlan {
775 pub fn new() -> Self {
777 Self { tasks: Vec::new() }
778 }
779
780 pub fn len(&self) -> usize {
782 self.tasks.len()
783 }
784
785 pub fn is_empty(&self) -> bool {
787 self.tasks.is_empty()
788 }
789
790 pub fn get_task(&self, id: &str) -> Option<&PlannedTask> {
792 self.tasks.iter().find(|t| t.id == id)
793 }
794
795 pub fn validate(&self) -> Result<(), String> {
797 if self.tasks.is_empty() {
798 return Err("Plan has no tasks".to_string());
799 }
800
801 let mut seen_ids = std::collections::HashSet::new();
803 for task in &self.tasks {
804 if !seen_ids.insert(&task.id) {
805 return Err(format!("Duplicate task ID: {}", task.id));
806 }
807 if task.goal.is_empty() {
808 return Err(format!("Task {} has empty goal", task.id));
809 }
810 }
811
812 for task in &self.tasks {
814 for dep in &task.dependencies {
815 if !seen_ids.contains(dep) {
816 return Err(format!("Task {} has unknown dependency: {}", task.id, dep));
817 }
818 }
819 }
820
821 let mut file_owners: std::collections::HashMap<&str, &str> =
823 std::collections::HashMap::new();
824 for task in &self.tasks {
825 for file in &task.output_files {
826 if let Some(prev_owner) = file_owners.insert(file.as_str(), task.id.as_str()) {
827 return Err(format!(
828 "Ownership violation in plan: file '{}' claimed by both '{}' and '{}'. \
829 Each output file must appear in exactly one task's output_files.",
830 file, prev_owner, task.id
831 ));
832 }
833 }
834 }
835
836 {
838 let id_to_idx: std::collections::HashMap<&str, usize> = self
839 .tasks
840 .iter()
841 .enumerate()
842 .map(|(i, t)| (t.id.as_str(), i))
843 .collect();
844 let n = self.tasks.len();
845 let mut in_degree = vec![0usize; n];
846 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
847 for (i, task) in self.tasks.iter().enumerate() {
848 for dep in &task.dependencies {
849 if let Some(&dep_idx) = id_to_idx.get(dep.as_str()) {
850 adj[dep_idx].push(i);
851 in_degree[i] += 1;
852 }
853 }
854 }
855 let mut queue: std::collections::VecDeque<usize> = in_degree
856 .iter()
857 .enumerate()
858 .filter_map(|(i, &d)| if d == 0 { Some(i) } else { None })
859 .collect();
860 let mut visited = 0usize;
861 while let Some(node) = queue.pop_front() {
862 visited += 1;
863 for &next in &adj[node] {
864 in_degree[next] -= 1;
865 if in_degree[next] == 0 {
866 queue.push_back(next);
867 }
868 }
869 }
870 if visited != n {
871 return Err("Plan contains a dependency cycle".to_string());
872 }
873 }
874
875 for task in &self.tasks {
878 for ctx_file in &task.context_files {
879 if let Some(&owner) = file_owners.get(ctx_file.as_str()) {
880 if owner != task.id && !task.dependencies.iter().any(|d| d == owner) {
881 return Err(format!(
882 "Task '{}' reads '{}' produced by '{}' but does not declare it as a dependency",
883 task.id, ctx_file, owner
884 ));
885 }
886 }
887 }
888 }
889
890 let registry = crate::plugin::PluginRegistry::new();
894 let all_test_patterns: Vec<&str> = registry
895 .all()
896 .iter()
897 .flat_map(|p| p.test_file_patterns().iter().copied())
898 .collect();
899 if !all_test_patterns.is_empty() {
900 let is_test_file = |path: &str| -> bool {
901 all_test_patterns
902 .iter()
903 .any(|pat| glob_matches_simple(pat, path))
904 };
905 let source_task_ids: Vec<&str> = self
907 .tasks
908 .iter()
909 .filter(|t| {
910 !t.output_files.is_empty() && t.output_files.iter().any(|f| !is_test_file(f))
911 })
912 .map(|t| t.id.as_str())
913 .collect();
914 for task in &self.tasks {
915 if task.output_files.is_empty() {
916 continue;
917 }
918 let all_tests = task.output_files.iter().all(|f| is_test_file(f));
919 if !all_tests {
920 continue;
921 }
922 for &src_id in &source_task_ids {
924 if src_id != task.id && !task.dependencies.iter().any(|d| d == src_id) {
925 return Err(format!(
926 "Test task '{}' produces only test files but does not depend on source task '{}'",
927 task.id, src_id
928 ));
929 }
930 }
931 }
932 }
933
934 Ok(())
935 }
936}
937
938fn glob_matches_simple(pattern: &str, path: &str) -> bool {
943 let pat_parts: Vec<&str> = pattern.split('/').collect();
944 let path_parts: Vec<&str> = path.split('/').collect();
945 glob_match_parts(&pat_parts, &path_parts)
946}
947
948fn glob_match_parts(pat: &[&str], path: &[&str]) -> bool {
949 if pat.is_empty() {
950 return path.is_empty();
951 }
952 if pat[0] == "**" {
953 for i in 0..=path.len() {
955 if glob_match_parts(&pat[1..], &path[i..]) {
956 return true;
957 }
958 }
959 return false;
960 }
961 if path.is_empty() {
962 return false;
963 }
964 if glob_match_component(pat[0], path[0]) {
965 glob_match_parts(&pat[1..], &path[1..])
966 } else {
967 false
968 }
969}
970
971fn glob_match_component(pattern: &str, component: &str) -> bool {
972 if pattern == "*" {
974 return true;
975 }
976 if !pattern.contains('*') {
977 return pattern == component;
978 }
979 let parts: Vec<&str> = pattern.split('*').collect();
980 let mut pos = 0;
981 for (i, part) in parts.iter().enumerate() {
982 if part.is_empty() {
983 continue;
984 }
985 if let Some(found) = component[pos..].find(part) {
986 if i == 0 && found != 0 {
987 return false; }
989 pos += found + part.len();
990 } else {
991 return false;
992 }
993 }
994 if let Some(last) = parts.last() {
995 if !last.is_empty() {
996 return component.ends_with(last);
997 }
998 }
999 true
1000}
1001
1002impl Default for TaskPlan {
1003 fn default() -> Self {
1004 Self::new()
1005 }
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1010pub struct PlannedTask {
1011 pub id: String,
1013 pub goal: String,
1015 #[serde(default)]
1017 pub context_files: Vec<String>,
1018 #[serde(default)]
1020 pub output_files: Vec<String>,
1021 #[serde(default)]
1023 pub dependencies: Vec<String>,
1024 #[serde(default)]
1026 pub task_type: TaskType,
1027 #[serde(default)]
1029 pub contract: PlannedContract,
1030 #[serde(default)]
1032 pub command_contract: Option<CommandContract>,
1033 #[serde(default)]
1035 pub node_class: NodeClass,
1036 #[serde(default)]
1038 pub dependency_expectations: DependencyExpectation,
1039}
1040
1041impl PlannedTask {
1042 pub fn new(id: impl Into<String>, goal: impl Into<String>) -> Self {
1044 Self {
1045 id: id.into(),
1046 goal: goal.into(),
1047 context_files: Vec::new(),
1048 output_files: Vec::new(),
1049 dependencies: Vec::new(),
1050 task_type: TaskType::Code,
1051 contract: PlannedContract::default(),
1052 command_contract: None,
1053 node_class: NodeClass::default(),
1054 dependency_expectations: DependencyExpectation::default(),
1055 }
1056 }
1057
1058 pub fn to_srbn_node(&self, tier: ModelTier) -> SRBNNode {
1060 let mut node = SRBNNode::new(self.id.clone(), self.goal.clone(), tier);
1061 node.context_files = self.context_files.iter().map(PathBuf::from).collect();
1062 node.output_targets = self.output_files.iter().map(PathBuf::from).collect();
1063 node.contract = self.contract.to_behavioral_contract();
1064 node.node_class = self.node_class;
1065 node.dependency_expectations = self.dependency_expectations.clone();
1066 node
1067 }
1068}
1069
1070#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1072pub struct PlannedContract {
1073 #[serde(default)]
1075 pub interface_signature: Option<String>,
1076 #[serde(default)]
1078 pub invariants: Vec<String>,
1079 #[serde(default)]
1081 pub forbidden_patterns: Vec<String>,
1082 #[serde(default)]
1084 pub tests: Vec<PlannedTest>,
1085}
1086
1087impl PlannedContract {
1088 pub fn to_behavioral_contract(&self) -> BehavioralContract {
1090 BehavioralContract {
1091 interface_signature: self.interface_signature.clone().unwrap_or_default(),
1092 invariants: self.invariants.clone(),
1093 forbidden_patterns: self.forbidden_patterns.clone(),
1094 weighted_tests: self
1095 .tests
1096 .iter()
1097 .map(|t| WeightedTest {
1098 test_name: t.name.clone(),
1099 criticality: t.criticality,
1100 })
1101 .collect(),
1102 energy_weights: (1.0, 0.5, 2.0),
1103 }
1104 }
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1109pub struct PlannedTest {
1110 pub name: String,
1112 #[serde(default = "default_criticality")]
1114 pub criticality: Criticality,
1115}
1116
1117fn default_criticality() -> Criticality {
1118 Criticality::High
1119}
1120
1121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1124pub struct CommandContract {
1125 pub command: String,
1127 #[serde(default)]
1129 pub expected_exit_code: i32,
1130 #[serde(default)]
1132 pub expected_files: Vec<String>,
1133 #[serde(default)]
1135 pub forbidden_stderr_patterns: Vec<String>,
1136 #[serde(default)]
1138 pub working_dir: Option<String>,
1139}
1140
1141impl CommandContract {
1142 pub fn new(command: impl Into<String>) -> Self {
1144 Self {
1145 command: command.into(),
1146 expected_exit_code: 0,
1147 expected_files: Vec::new(),
1148 forbidden_stderr_patterns: Vec::new(),
1149 working_dir: None,
1150 }
1151 }
1152
1153 pub fn calculate_energy(&self, exit_code: i32, stderr: &str, existing_files: &[String]) -> f32 {
1155 let mut energy = 0.0;
1156
1157 if exit_code != self.expected_exit_code {
1159 energy += 1.0;
1160 }
1161
1162 for expected in &self.expected_files {
1164 if !existing_files.contains(expected) {
1165 energy += 0.5;
1166 }
1167 }
1168
1169 for pattern in &self.forbidden_stderr_patterns {
1171 if stderr.contains(pattern) {
1172 energy += 0.3;
1173 }
1174 }
1175
1176 energy
1177 }
1178}
1179
1180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1189#[serde(rename_all = "snake_case")]
1190pub enum ExecutionMode {
1191 #[default]
1193 Project,
1194 Solo,
1196}
1197
1198impl std::fmt::Display for ExecutionMode {
1199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1200 match self {
1201 ExecutionMode::Project => write!(f, "project"),
1202 ExecutionMode::Solo => write!(f, "solo"),
1203 }
1204 }
1205}
1206
1207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
1213#[serde(rename_all = "snake_case")]
1214pub enum WorkspaceState {
1215 ExistingProject {
1217 plugins: Vec<String>,
1219 },
1220 Greenfield {
1222 inferred_lang: Option<String>,
1224 },
1225 #[default]
1227 Ambiguous,
1228}
1229
1230impl std::fmt::Display for WorkspaceState {
1231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1232 match self {
1233 WorkspaceState::ExistingProject { plugins } => {
1234 write!(f, "existing-project({})", plugins.join(", "))
1235 }
1236 WorkspaceState::Greenfield { inferred_lang } => {
1237 write!(
1238 f,
1239 "greenfield({})",
1240 inferred_lang.as_deref().unwrap_or("unknown")
1241 )
1242 }
1243 WorkspaceState::Ambiguous => write!(f, "ambiguous"),
1244 }
1245 }
1246}
1247
1248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1254#[serde(rename_all = "snake_case")]
1255pub enum NodeClass {
1256 Interface,
1258 #[default]
1260 Implementation,
1261 Integration,
1263}
1264
1265impl std::fmt::Display for NodeClass {
1266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1267 match self {
1268 NodeClass::Interface => write!(f, "interface"),
1269 NodeClass::Implementation => write!(f, "implementation"),
1270 NodeClass::Integration => write!(f, "integration"),
1271 }
1272 }
1273}
1274
1275#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1279#[serde(rename_all = "snake_case")]
1280pub enum VerifierStrictness {
1281 #[default]
1283 Default,
1284 Strict,
1286 Minimal,
1288}
1289
1290#[derive(Debug, Clone, Serialize, Deserialize)]
1296pub struct OwnershipEntry {
1297 pub owner_node_id: String,
1299 pub owner_plugin: String,
1301 pub node_class: NodeClass,
1303}
1304
1305#[derive(Debug, Clone, Serialize, Deserialize)]
1310pub struct OwnershipManifest {
1311 entries: std::collections::HashMap<String, OwnershipEntry>,
1313 #[serde(default = "OwnershipManifest::default_fanout")]
1315 fanout_limit: usize,
1316}
1317
1318impl Default for OwnershipManifest {
1319 fn default() -> Self {
1320 Self::new()
1321 }
1322}
1323
1324impl OwnershipManifest {
1325 pub fn new() -> Self {
1327 Self {
1328 entries: std::collections::HashMap::new(),
1329 fanout_limit: Self::default_fanout(),
1330 }
1331 }
1332
1333 pub fn with_fanout_limit(limit: usize) -> Self {
1335 Self {
1336 entries: std::collections::HashMap::new(),
1337 fanout_limit: limit,
1338 }
1339 }
1340
1341 fn default_fanout() -> usize {
1342 20
1343 }
1344
1345 pub fn assign(
1350 &mut self,
1351 path: impl Into<String>,
1352 owner_node_id: impl Into<String>,
1353 owner_plugin: impl Into<String>,
1354 node_class: NodeClass,
1355 ) {
1356 let key = crate::path::normalize_path_key(&path.into()).unwrap_or_default();
1357 if key.is_empty() {
1358 return; }
1360 self.entries.insert(
1361 key,
1362 OwnershipEntry {
1363 owner_node_id: owner_node_id.into(),
1364 owner_plugin: owner_plugin.into(),
1365 node_class,
1366 },
1367 );
1368 }
1369
1370 pub fn owner_of(&self, path: &str) -> Option<&OwnershipEntry> {
1374 let key = crate::path::normalize_path_key(path)?;
1375 self.entries.get(&key)
1376 }
1377
1378 pub fn files_owned_by(&self, node_id: &str) -> Vec<&str> {
1380 self.entries
1381 .iter()
1382 .filter(|(_, entry)| entry.owner_node_id == node_id)
1383 .map(|(path, _)| path.as_str())
1384 .collect()
1385 }
1386
1387 pub fn len(&self) -> usize {
1389 self.entries.len()
1390 }
1391
1392 pub fn is_empty(&self) -> bool {
1394 self.entries.is_empty()
1395 }
1396
1397 pub fn fanout_limit(&self) -> usize {
1399 self.fanout_limit
1400 }
1401
1402 pub fn validate_bundle(
1411 &self,
1412 bundle: &ArtifactBundle,
1413 node_id: &str,
1414 node_class: NodeClass,
1415 ) -> Result<(), String> {
1416 let artifact_count = bundle.len();
1417
1418 if artifact_count > self.fanout_limit {
1420 return Err(format!(
1421 "Bundle has {} artifacts, exceeding fanout limit of {}",
1422 artifact_count, self.fanout_limit
1423 ));
1424 }
1425
1426 if node_class == NodeClass::Integration {
1428 return Ok(());
1429 }
1430
1431 for op in &bundle.artifacts {
1433 let raw_path = op.path();
1434 let key =
1435 crate::path::normalize_path_key(raw_path).unwrap_or_else(|| raw_path.to_string());
1436 if let Some(entry) = self.entries.get(&key) {
1437 if entry.owner_node_id != node_id {
1438 return Err(format!(
1439 "Ownership violation: file '{}' is owned by node '{}', \
1440 but node '{}' ({}) attempted to modify it. \
1441 Only Integration nodes may cross ownership boundaries.",
1442 raw_path, entry.owner_node_id, node_id, node_class
1443 ));
1444 }
1445 }
1446 }
1448
1449 Ok(())
1450 }
1451
1452 pub fn assign_new_paths(
1457 &mut self,
1458 bundle: &ArtifactBundle,
1459 node_id: &str,
1460 owner_plugin: &str,
1461 node_class: NodeClass,
1462 ) {
1463 for op in &bundle.artifacts {
1464 let raw_path = op.path();
1465 let key =
1466 crate::path::normalize_path_key(raw_path).unwrap_or_else(|| raw_path.to_string());
1467 if !self.entries.contains_key(&key) {
1468 self.assign(raw_path, node_id, owner_plugin, node_class);
1469 }
1470 }
1471 }
1472}
1473
1474#[derive(Debug, Clone, Serialize, Deserialize)]
1478#[serde(tag = "operation", rename_all = "snake_case")]
1479pub enum ArtifactOperation {
1480 Write {
1482 path: String,
1484 content: String,
1486 },
1487 Diff {
1489 path: String,
1491 patch: String,
1493 },
1494 Delete {
1496 path: String,
1498 },
1499 Move {
1501 from: String,
1503 to: String,
1505 },
1506}
1507
1508impl ArtifactOperation {
1509 pub fn path(&self) -> &str {
1511 match self {
1512 ArtifactOperation::Write { path, .. } => path,
1513 ArtifactOperation::Diff { path, .. } => path,
1514 ArtifactOperation::Delete { path } => path,
1515 ArtifactOperation::Move { from, .. } => from,
1516 }
1517 }
1518
1519 pub fn is_write(&self) -> bool {
1521 matches!(self, ArtifactOperation::Write { .. })
1522 }
1523
1524 pub fn is_diff(&self) -> bool {
1526 matches!(self, ArtifactOperation::Diff { .. })
1527 }
1528
1529 pub fn is_delete(&self) -> bool {
1531 matches!(self, ArtifactOperation::Delete { .. })
1532 }
1533
1534 pub fn is_move(&self) -> bool {
1536 matches!(self, ArtifactOperation::Move { .. })
1537 }
1538}
1539
1540#[derive(Debug, Clone, Serialize, Deserialize)]
1546pub struct ArtifactBundle {
1547 pub artifacts: Vec<ArtifactOperation>,
1549 #[serde(default)]
1551 pub commands: Vec<String>,
1552}
1553
1554impl ArtifactBundle {
1555 pub fn new() -> Self {
1557 Self {
1558 artifacts: Vec::new(),
1559 commands: Vec::new(),
1560 }
1561 }
1562
1563 pub fn len(&self) -> usize {
1565 self.artifacts.len()
1566 }
1567
1568 pub fn is_empty(&self) -> bool {
1570 self.artifacts.is_empty()
1571 }
1572
1573 pub fn affected_paths(&self) -> Vec<&str> {
1575 let mut paths: Vec<&str> = self.artifacts.iter().map(|a| a.path()).collect();
1576 for op in &self.artifacts {
1578 if let ArtifactOperation::Move { to, .. } = op {
1579 paths.push(to.as_str());
1580 }
1581 }
1582 paths.sort();
1583 paths.dedup();
1584 paths
1585 }
1586
1587 pub fn writes_count(&self) -> usize {
1589 self.artifacts.iter().filter(|a| a.is_write()).count()
1590 }
1591
1592 pub fn diffs_count(&self) -> usize {
1594 self.artifacts.iter().filter(|a| a.is_diff()).count()
1595 }
1596
1597 pub fn deletes_count(&self) -> usize {
1599 self.artifacts.iter().filter(|a| a.is_delete()).count()
1600 }
1601
1602 pub fn moves_count(&self) -> usize {
1604 self.artifacts.iter().filter(|a| a.is_move()).count()
1605 }
1606
1607 pub fn validate(&self) -> Result<(), String> {
1609 if self.artifacts.is_empty() {
1610 return Err("Artifact bundle is empty".to_string());
1611 }
1612
1613 for (i, op) in self.artifacts.iter().enumerate() {
1614 Self::validate_path(op.path(), i)?;
1616
1617 if let ArtifactOperation::Move { to, .. } = op {
1619 if to.is_empty() {
1620 return Err(format!("Artifact {} (move) has empty destination path", i));
1621 }
1622 Self::validate_path(to, i)?;
1623 }
1624 }
1625
1626 Ok(())
1627 }
1628
1629 fn validate_path(path: &str, artifact_index: usize) -> Result<(), String> {
1635 use crate::path::{normalize_artifact_path, PathError};
1636 match normalize_artifact_path(path) {
1637 Ok(_) => Ok(()),
1638 Err(PathError::Empty) => Err(format!("Artifact {} has empty path", artifact_index)),
1639 Err(PathError::Absolute(_)) => Err(format!(
1640 "Artifact {} has absolute path '{}', must be relative",
1641 artifact_index, path
1642 )),
1643 Err(PathError::Escapes(_)) => Err(format!(
1644 "Artifact {} has path traversal in '{}'",
1645 artifact_index, path
1646 )),
1647 Err(PathError::Invalid(_)) => Err(format!(
1648 "Artifact {} has invalid path '{}'",
1649 artifact_index, path
1650 )),
1651 }
1652 }
1653}
1654
1655impl Default for ArtifactBundle {
1656 fn default() -> Self {
1657 Self::new()
1658 }
1659}
1660
1661#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1666pub struct VerificationResult {
1667 pub syntax_ok: bool,
1669 pub build_ok: bool,
1671 pub tests_ok: bool,
1673 pub lint_ok: bool,
1675 pub diagnostics_count: usize,
1677 pub tests_passed: usize,
1679 pub tests_failed: usize,
1681 pub summary: String,
1683 pub raw_output: Option<String>,
1685 pub degraded: bool,
1687 pub degraded_reason: Option<String>,
1689 #[serde(default)]
1691 pub stage_outcomes: Vec<StageOutcome>,
1692}
1693
1694impl VerificationResult {
1695 pub fn all_passed(&self) -> bool {
1697 self.syntax_ok && self.build_ok && self.tests_ok && !self.degraded
1698 }
1699
1700 pub fn degraded(reason: impl Into<String>) -> Self {
1702 Self {
1703 degraded: true,
1704 degraded_reason: Some(reason.into()),
1705 summary: "Verification ran in degraded mode".to_string(),
1706 ..Default::default()
1707 }
1708 }
1709
1710 pub fn has_degraded_stages(&self) -> bool {
1715 self.stage_outcomes
1716 .iter()
1717 .any(|s| !matches!(s.sensor_status, SensorStatus::Available))
1718 }
1719
1720 pub fn degraded_stage_reasons(&self) -> Vec<String> {
1722 self.stage_outcomes
1723 .iter()
1724 .filter_map(|s| match &s.sensor_status {
1725 SensorStatus::Available => None,
1726 SensorStatus::Fallback { actual, reason } => Some(format!(
1727 "{}: used fallback '{}' ({})",
1728 s.stage, actual, reason
1729 )),
1730 SensorStatus::Unavailable { reason } => {
1731 Some(format!("{}: unavailable ({})", s.stage, reason))
1732 }
1733 })
1734 .collect()
1735 }
1736}
1737
1738#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1743pub enum SensorStatus {
1744 Available,
1746 Fallback {
1748 actual: String,
1750 reason: String,
1752 },
1753 Unavailable {
1755 reason: String,
1757 },
1758}
1759
1760impl std::fmt::Display for SensorStatus {
1761 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1762 match self {
1763 SensorStatus::Available => write!(f, "available"),
1764 SensorStatus::Fallback { actual, .. } => write!(f, "fallback({})", actual),
1765 SensorStatus::Unavailable { reason } => write!(f, "unavailable({})", reason),
1766 }
1767 }
1768}
1769
1770#[derive(Debug, Clone, Serialize, Deserialize)]
1772pub struct StageOutcome {
1773 pub stage: String,
1775 pub passed: bool,
1777 pub sensor_status: SensorStatus,
1779 pub output: Option<String>,
1781}
1782
1783#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1789#[serde(rename_all = "snake_case")]
1790pub enum ArtifactKind {
1791 Signature,
1793 Schema,
1795 SymbolInventory,
1797 InterfaceSeal,
1799}
1800
1801impl std::fmt::Display for ArtifactKind {
1802 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1803 match self {
1804 ArtifactKind::Signature => write!(f, "signature"),
1805 ArtifactKind::Schema => write!(f, "schema"),
1806 ArtifactKind::SymbolInventory => write!(f, "symbol_inventory"),
1807 ArtifactKind::InterfaceSeal => write!(f, "interface_seal"),
1808 }
1809 }
1810}
1811
1812#[derive(Debug, Clone, Serialize, Deserialize)]
1818pub struct StructuralDigest {
1819 pub digest_id: String,
1821 pub artifact_kind: ArtifactKind,
1823 pub hash: [u8; 32],
1825 pub source_node_id: String,
1827 pub source_path: String,
1829 pub version: u32,
1831}
1832
1833impl StructuralDigest {
1834 pub fn from_content(
1836 source_node_id: impl Into<String>,
1837 source_path: impl Into<String>,
1838 artifact_kind: ArtifactKind,
1839 content: &[u8],
1840 ) -> Self {
1841 use std::collections::hash_map::DefaultHasher;
1842 use std::hash::{Hash, Hasher};
1843
1844 let mut sha = [0u8; 32];
1845 let mut hasher = DefaultHasher::new();
1847 content.hash(&mut hasher);
1848 let h = hasher.finish().to_le_bytes();
1849 sha[..8].copy_from_slice(&h);
1850
1851 let node_id = source_node_id.into();
1852 let path = source_path.into();
1853 let digest_id = format!("{}:{}:{}", node_id, path, artifact_kind);
1854
1855 Self {
1856 digest_id,
1857 artifact_kind,
1858 hash: sha,
1859 source_node_id: node_id,
1860 source_path: path,
1861 version: 1,
1862 }
1863 }
1864
1865 pub fn matches(&self, other: &Self) -> bool {
1867 self.hash == other.hash
1868 }
1869}
1870
1871#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1873#[serde(rename_all = "snake_case")]
1874pub enum SummaryKind {
1875 IntentSummary,
1877 VerifierResults,
1879 DesignRationale,
1881}
1882
1883#[derive(Debug, Clone, Serialize, Deserialize)]
1888pub struct SummaryDigest {
1889 pub digest_id: String,
1891 pub source_node_id: String,
1893 pub kind: SummaryKind,
1895 pub hash: [u8; 32],
1897 pub original_byte_length: usize,
1899 pub summary_text: String,
1901}
1902
1903#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1905pub struct ContextBudget {
1906 pub byte_limit: usize,
1908 pub file_count_limit: usize,
1910}
1911
1912impl Default for ContextBudget {
1913 fn default() -> Self {
1914 Self {
1915 byte_limit: 100 * 1024, file_count_limit: 20,
1917 }
1918 }
1919}
1920
1921#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1927pub struct RestrictionMap {
1928 pub node_id: String,
1930 #[serde(default)]
1932 pub budget: ContextBudget,
1933 #[serde(default)]
1935 pub owned_files: Vec<String>,
1936 #[serde(default)]
1938 pub sealed_interfaces: Vec<String>,
1939 #[serde(default)]
1941 pub structural_digests: Vec<StructuralDigest>,
1942 #[serde(default)]
1944 pub summary_digests: Vec<SummaryDigest>,
1945 #[serde(default)]
1947 pub dependency_commits: std::collections::HashMap<String, Vec<u8>>,
1948}
1949
1950impl RestrictionMap {
1951 pub fn for_node(node_id: impl Into<String>) -> Self {
1953 Self {
1954 node_id: node_id.into(),
1955 ..Default::default()
1956 }
1957 }
1958
1959 pub fn structural_bytes(&self) -> usize {
1961 self.structural_digests
1962 .iter()
1963 .map(|d| d.source_path.len() + 64)
1964 .sum::<usize>()
1965 + self.sealed_interfaces.len() * 128
1966 }
1967}
1968
1969#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1975pub struct ContextPackage {
1976 pub package_id: String,
1978 pub node_id: String,
1980 pub restriction_map: RestrictionMap,
1982 #[serde(default)]
1984 pub included_files: std::collections::HashMap<String, String>,
1985 #[serde(default)]
1987 pub structural_digests: Vec<StructuralDigest>,
1988 #[serde(default)]
1990 pub summary_digests: Vec<SummaryDigest>,
1991 pub total_bytes: usize,
1993 pub budget_exceeded: bool,
1995 pub created_at: i64,
1997}
1998
1999impl ContextPackage {
2000 pub fn new(node_id: impl Into<String>) -> Self {
2002 let nid = node_id.into();
2003 let ts = std::time::SystemTime::now()
2004 .duration_since(std::time::UNIX_EPOCH)
2005 .unwrap_or_default()
2006 .as_secs() as i64;
2007 Self {
2008 package_id: format!("ctx_{}_{}", nid, ts),
2009 node_id: nid,
2010 created_at: ts,
2011 ..Default::default()
2012 }
2013 }
2014
2015 pub fn add_file(&mut self, path: &str, content: String) -> bool {
2017 let new_bytes = self.total_bytes + content.len();
2018 if new_bytes > self.restriction_map.budget.byte_limit {
2019 self.budget_exceeded = true;
2020 return false;
2021 }
2022 if self.included_files.len() >= self.restriction_map.budget.file_count_limit {
2023 self.budget_exceeded = true;
2024 return false;
2025 }
2026 self.total_bytes = new_bytes;
2027 self.included_files.insert(path.to_string(), content);
2028 true
2029 }
2030
2031 pub fn add_structural_digest(&mut self, digest: StructuralDigest) {
2033 self.structural_digests.push(digest);
2034 }
2035
2036 pub fn add_summary_digest(&mut self, digest: SummaryDigest) {
2038 self.total_bytes += digest.summary_text.len();
2039 self.summary_digests.push(digest);
2040 }
2041
2042 pub fn provenance(&self) -> ContextProvenance {
2044 ContextProvenance {
2045 node_id: self.node_id.clone(),
2046 context_package_id: self.package_id.clone(),
2047 structural_digest_hashes: self
2048 .structural_digests
2049 .iter()
2050 .map(|d| (d.digest_id.clone(), d.hash))
2051 .collect(),
2052 summary_digest_hashes: self
2053 .summary_digests
2054 .iter()
2055 .map(|d| (d.digest_id.clone(), d.hash))
2056 .collect(),
2057 dependency_commit_hashes: self
2058 .restriction_map
2059 .dependency_commits
2060 .iter()
2061 .map(|(k, v)| (k.clone(), v.clone()))
2062 .collect(),
2063 included_file_count: self.included_files.len(),
2064 total_bytes: self.total_bytes,
2065 created_at: self.created_at,
2066 }
2067 }
2068}
2069
2070#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2076pub struct ContextProvenance {
2077 pub node_id: String,
2079 pub context_package_id: String,
2081 #[serde(default)]
2083 pub structural_digest_hashes: Vec<(String, [u8; 32])>,
2084 #[serde(default)]
2086 pub summary_digest_hashes: Vec<(String, [u8; 32])>,
2087 #[serde(default)]
2089 pub dependency_commit_hashes: Vec<(String, Vec<u8>)>,
2090 pub included_file_count: usize,
2092 pub total_bytes: usize,
2094 pub created_at: i64,
2096}
2097
2098#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2108#[serde(rename_all = "snake_case")]
2109pub enum EscalationCategory {
2110 ImplementationError,
2112 ContractMismatch,
2114 InsufficientModelCapability,
2116 DegradedSensors,
2118 TopologyMismatch,
2120}
2121
2122impl std::fmt::Display for EscalationCategory {
2123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2124 match self {
2125 EscalationCategory::ImplementationError => write!(f, "implementation_error"),
2126 EscalationCategory::ContractMismatch => write!(f, "contract_mismatch"),
2127 EscalationCategory::InsufficientModelCapability => {
2128 write!(f, "insufficient_model_capability")
2129 }
2130 EscalationCategory::DegradedSensors => write!(f, "degraded_sensors"),
2131 EscalationCategory::TopologyMismatch => write!(f, "topology_mismatch"),
2132 }
2133 }
2134}
2135
2136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2143#[serde(rename_all = "snake_case")]
2144pub enum RewriteAction {
2145 GroundedRetry {
2147 evidence_summary: String,
2149 },
2150 ContractRepair {
2152 fields: Vec<String>,
2154 },
2155 CapabilityPromotion {
2157 from_tier: ModelTier,
2159 to_tier: ModelTier,
2161 },
2162 SensorRecovery {
2164 degraded_stages: Vec<String>,
2166 },
2167 DegradedValidationStop {
2170 reason: String,
2172 },
2173 NodeSplit {
2175 proposed_children: Vec<String>,
2177 },
2178 InterfaceInsertion {
2180 boundary: String,
2182 },
2183 SubgraphReplan {
2185 affected_nodes: Vec<String>,
2187 },
2188 UserEscalation {
2190 evidence: String,
2192 },
2193}
2194
2195impl std::fmt::Display for RewriteAction {
2196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2197 match self {
2198 RewriteAction::GroundedRetry { .. } => write!(f, "grounded_retry"),
2199 RewriteAction::ContractRepair { .. } => write!(f, "contract_repair"),
2200 RewriteAction::CapabilityPromotion { .. } => write!(f, "capability_promotion"),
2201 RewriteAction::SensorRecovery { .. } => write!(f, "sensor_recovery"),
2202 RewriteAction::DegradedValidationStop { .. } => {
2203 write!(f, "degraded_validation_stop")
2204 }
2205 RewriteAction::NodeSplit { .. } => write!(f, "node_split"),
2206 RewriteAction::InterfaceInsertion { .. } => write!(f, "interface_insertion"),
2207 RewriteAction::SubgraphReplan { .. } => write!(f, "subgraph_replan"),
2208 RewriteAction::UserEscalation { .. } => write!(f, "user_escalation"),
2209 }
2210 }
2211}
2212
2213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2218#[serde(rename_all = "snake_case")]
2219pub enum SheafValidatorClass {
2220 ExportImportConsistency,
2222 DependencyGraphConsistency,
2225 SchemaContractCompatibility,
2227 BuildGraphConsistency,
2229 TestOwnershipConsistency,
2231 CrossLanguageBoundary,
2233 PolicyInvariantConsistency,
2235}
2236
2237impl std::fmt::Display for SheafValidatorClass {
2238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2239 match self {
2240 SheafValidatorClass::ExportImportConsistency => write!(f, "export_import"),
2241 SheafValidatorClass::DependencyGraphConsistency => write!(f, "dependency_graph"),
2242 SheafValidatorClass::SchemaContractCompatibility => write!(f, "schema_contract"),
2243 SheafValidatorClass::BuildGraphConsistency => write!(f, "build_graph"),
2244 SheafValidatorClass::TestOwnershipConsistency => write!(f, "test_ownership"),
2245 SheafValidatorClass::CrossLanguageBoundary => write!(f, "cross_language"),
2246 SheafValidatorClass::PolicyInvariantConsistency => write!(f, "policy_invariant"),
2247 }
2248 }
2249}
2250
2251#[derive(Debug, Clone, Serialize, Deserialize)]
2253pub struct SheafValidationResult {
2254 pub validator_class: SheafValidatorClass,
2256 pub plugin_source: Option<String>,
2258 pub passed: bool,
2260 pub validated_boundaries: Vec<String>,
2262 pub evidence_summary: String,
2264 pub affected_files: Vec<String>,
2266 pub v_sheaf_contribution: f32,
2268 pub requeue_targets: Vec<String>,
2270}
2271
2272impl SheafValidationResult {
2273 pub fn passed(class: SheafValidatorClass, boundaries: Vec<String>) -> Self {
2275 Self {
2276 validator_class: class,
2277 plugin_source: None,
2278 passed: true,
2279 validated_boundaries: boundaries,
2280 evidence_summary: String::new(),
2281 affected_files: Vec::new(),
2282 v_sheaf_contribution: 0.0,
2283 requeue_targets: Vec::new(),
2284 }
2285 }
2286
2287 pub fn failed(
2289 class: SheafValidatorClass,
2290 evidence: impl Into<String>,
2291 affected: Vec<String>,
2292 requeue: Vec<String>,
2293 v_sheaf: f32,
2294 ) -> Self {
2295 Self {
2296 validator_class: class,
2297 plugin_source: None,
2298 passed: false,
2299 validated_boundaries: Vec::new(),
2300 evidence_summary: evidence.into(),
2301 affected_files: affected,
2302 v_sheaf_contribution: v_sheaf,
2303 requeue_targets: requeue,
2304 }
2305 }
2306}
2307
2308#[derive(Debug, Clone, Serialize, Deserialize)]
2314pub struct EscalationReport {
2315 pub node_id: String,
2317 pub session_id: String,
2319 pub category: EscalationCategory,
2321 pub action: RewriteAction,
2323 pub energy_snapshot: EnergyComponents,
2325 pub stage_outcomes: Vec<StageOutcome>,
2327 pub evidence: String,
2329 pub affected_node_ids: Vec<String>,
2331 pub timestamp: i64,
2333}
2334
2335#[derive(Debug, Clone, Serialize, Deserialize)]
2339pub struct RewriteRecord {
2340 pub node_id: String,
2342 pub session_id: String,
2344 pub action: RewriteAction,
2346 pub category: EscalationCategory,
2348 pub requeued_nodes: Vec<String>,
2350 pub inserted_nodes: Vec<String>,
2352 pub timestamp: i64,
2354}
2355
2356#[derive(Debug, Clone, Serialize, Deserialize)]
2361pub struct TargetedRequeue {
2362 pub node_ids: Vec<String>,
2364 pub reason: String,
2366 pub evidence: String,
2368 pub sheaf_results: Vec<SheafValidationResult>,
2370 pub timestamp: i64,
2372}
2373
2374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2384#[serde(rename_all = "snake_case")]
2385pub enum ProvisionalBranchState {
2386 Active,
2388 Sealed,
2390 Merged,
2392 Flushed,
2394}
2395
2396impl std::fmt::Display for ProvisionalBranchState {
2397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2398 match self {
2399 ProvisionalBranchState::Active => write!(f, "active"),
2400 ProvisionalBranchState::Sealed => write!(f, "sealed"),
2401 ProvisionalBranchState::Merged => write!(f, "merged"),
2402 ProvisionalBranchState::Flushed => write!(f, "flushed"),
2403 }
2404 }
2405}
2406
2407#[derive(Debug, Clone, Serialize, Deserialize)]
2412pub struct ProvisionalBranch {
2413 pub branch_id: String,
2415 pub session_id: String,
2417 pub node_id: String,
2419 pub parent_node_id: String,
2421 pub state: ProvisionalBranchState,
2423 pub parent_seal_hash: Option<[u8; 32]>,
2426 pub sandbox_dir: Option<String>,
2428 pub created_at: i64,
2430 pub updated_at: i64,
2432}
2433
2434impl ProvisionalBranch {
2435 pub fn new(
2437 branch_id: impl Into<String>,
2438 session_id: impl Into<String>,
2439 node_id: impl Into<String>,
2440 parent_node_id: impl Into<String>,
2441 ) -> Self {
2442 let now = epoch_secs();
2443 Self {
2444 branch_id: branch_id.into(),
2445 session_id: session_id.into(),
2446 node_id: node_id.into(),
2447 parent_node_id: parent_node_id.into(),
2448 state: ProvisionalBranchState::Active,
2449 parent_seal_hash: None,
2450 sandbox_dir: None,
2451 created_at: now,
2452 updated_at: now,
2453 }
2454 }
2455
2456 pub fn is_live(&self) -> bool {
2458 matches!(
2459 self.state,
2460 ProvisionalBranchState::Active | ProvisionalBranchState::Sealed
2461 )
2462 }
2463
2464 pub fn is_flushed(&self) -> bool {
2466 self.state == ProvisionalBranchState::Flushed
2467 }
2468}
2469
2470#[derive(Debug, Clone, Serialize, Deserialize)]
2476pub struct BranchLineage {
2477 pub lineage_id: String,
2479 pub parent_branch_id: String,
2481 pub child_branch_id: String,
2483 pub depends_on_seal: bool,
2485}
2486
2487#[derive(Debug, Clone, Serialize, Deserialize)]
2494pub struct InterfaceSealRecord {
2495 pub seal_id: String,
2497 pub session_id: String,
2499 pub node_id: String,
2501 pub sealed_path: String,
2503 pub artifact_kind: ArtifactKind,
2505 pub seal_hash: [u8; 32],
2507 pub version: u32,
2509 pub created_at: i64,
2511}
2512
2513impl InterfaceSealRecord {
2514 pub fn from_digest(
2516 session_id: impl Into<String>,
2517 node_id: impl Into<String>,
2518 digest: &StructuralDigest,
2519 ) -> Self {
2520 let nid = node_id.into();
2521 let sid = session_id.into();
2522 let seal_id = format!("seal_{}_{}", nid, digest.source_path);
2523 Self {
2524 seal_id,
2525 session_id: sid,
2526 node_id: nid,
2527 sealed_path: digest.source_path.clone(),
2528 artifact_kind: digest.artifact_kind,
2529 seal_hash: digest.hash,
2530 version: digest.version,
2531 created_at: epoch_secs(),
2532 }
2533 }
2534
2535 pub fn matches_hash(&self, hash: &[u8; 32]) -> bool {
2537 self.seal_hash == *hash
2538 }
2539}
2540
2541#[derive(Debug, Clone, Serialize, Deserialize)]
2546pub struct BranchFlushRecord {
2547 pub flush_id: String,
2549 pub session_id: String,
2551 pub parent_node_id: String,
2553 pub flushed_branch_ids: Vec<String>,
2555 pub requeue_node_ids: Vec<String>,
2557 pub reason: String,
2559 pub created_at: i64,
2561}
2562
2563impl BranchFlushRecord {
2564 pub fn new(
2566 session_id: impl Into<String>,
2567 parent_node_id: impl Into<String>,
2568 flushed_branch_ids: Vec<String>,
2569 requeue_node_ids: Vec<String>,
2570 reason: impl Into<String>,
2571 ) -> Self {
2572 Self {
2573 flush_id: format!("flush_{}", uuid_v4()),
2574 session_id: session_id.into(),
2575 parent_node_id: parent_node_id.into(),
2576 flushed_branch_ids,
2577 requeue_node_ids,
2578 reason: reason.into(),
2579 created_at: epoch_secs(),
2580 }
2581 }
2582}
2583
2584#[derive(Debug, Clone, Serialize, Deserialize)]
2590pub struct BlockedDependency {
2591 pub child_node_id: String,
2593 pub parent_node_id: String,
2595 pub required_seal_paths: Vec<String>,
2597 pub blocked_at: i64,
2599}
2600
2601impl BlockedDependency {
2602 pub fn new(
2604 child_node_id: impl Into<String>,
2605 parent_node_id: impl Into<String>,
2606 required_seal_paths: Vec<String>,
2607 ) -> Self {
2608 Self {
2609 child_node_id: child_node_id.into(),
2610 parent_node_id: parent_node_id.into(),
2611 required_seal_paths,
2612 blocked_at: epoch_secs(),
2613 }
2614 }
2615}
2616
2617#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2627#[serde(rename_all = "snake_case")]
2628pub enum PlanRevisionStatus {
2629 #[default]
2631 Active,
2632 Superseded,
2634 Cancelled,
2636}
2637
2638impl std::fmt::Display for PlanRevisionStatus {
2639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2640 match self {
2641 Self::Active => write!(f, "active"),
2642 Self::Superseded => write!(f, "superseded"),
2643 Self::Cancelled => write!(f, "cancelled"),
2644 }
2645 }
2646}
2647
2648#[derive(Debug, Clone, Serialize, Deserialize)]
2655pub struct PlanRevision {
2656 pub revision_id: String,
2658 pub session_id: String,
2660 pub sequence: u32,
2662 pub plan: TaskPlan,
2664 pub reason: String,
2667 pub supersedes: Option<String>,
2669 pub status: PlanRevisionStatus,
2671 pub created_at: i64,
2673}
2674
2675impl PlanRevision {
2676 pub fn initial(session_id: impl Into<String>, plan: TaskPlan) -> Self {
2678 Self {
2679 revision_id: uuid_v4(),
2680 session_id: session_id.into(),
2681 sequence: 1,
2682 plan,
2683 reason: "initial".to_string(),
2684 supersedes: None,
2685 status: PlanRevisionStatus::Active,
2686 created_at: epoch_secs(),
2687 }
2688 }
2689
2690 pub fn successor(previous: &PlanRevision, plan: TaskPlan, reason: impl Into<String>) -> Self {
2692 Self {
2693 revision_id: uuid_v4(),
2694 session_id: previous.session_id.clone(),
2695 sequence: previous.sequence + 1,
2696 plan,
2697 reason: reason.into(),
2698 supersedes: Some(previous.revision_id.clone()),
2699 status: PlanRevisionStatus::Active,
2700 created_at: epoch_secs(),
2701 }
2702 }
2703
2704 pub fn is_active(&self) -> bool {
2706 self.status == PlanRevisionStatus::Active
2707 }
2708}
2709
2710#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
2720pub enum PlanningPolicy {
2721 LocalEdit,
2723 #[default]
2725 FeatureIncrement,
2726 LargeFeature,
2728 GreenfieldBuild,
2730 ArchitecturalRevision,
2732}
2733
2734impl PlanningPolicy {
2735 pub fn needs_architect(&self) -> bool {
2737 !matches!(self, Self::LocalEdit)
2738 }
2739
2740 pub fn needs_speculator(&self) -> bool {
2742 matches!(
2743 self,
2744 Self::LargeFeature | Self::GreenfieldBuild | Self::ArchitecturalRevision
2745 )
2746 }
2747}
2748
2749#[derive(Debug, Clone, Serialize, Deserialize)]
2755pub struct FeatureCharter {
2756 pub charter_id: String,
2758 pub session_id: String,
2760 pub scope_description: String,
2762 pub max_modules: Option<u32>,
2764 pub max_files: Option<u32>,
2766 pub max_revisions: Option<u32>,
2768 pub language_constraint: Option<String>,
2770 pub created_at: i64,
2772}
2773
2774impl FeatureCharter {
2775 pub fn new(session_id: impl Into<String>, scope_description: impl Into<String>) -> Self {
2777 Self {
2778 charter_id: uuid_v4(),
2779 session_id: session_id.into(),
2780 scope_description: scope_description.into(),
2781 max_modules: None,
2782 max_files: None,
2783 max_revisions: None,
2784 language_constraint: None,
2785 created_at: epoch_secs(),
2786 }
2787 }
2788}
2789
2790#[derive(Debug, Clone, Serialize, Deserialize)]
2796pub struct RepairFootprint {
2797 pub footprint_id: String,
2799 pub session_id: String,
2801 pub node_id: String,
2803 pub revision_id: String,
2805 pub attempt: u32,
2807 pub affected_files: Vec<String>,
2809 pub applied_bundle: ArtifactBundle,
2811 pub diagnosis: String,
2813 pub resolved: bool,
2815 pub created_at: i64,
2817}
2818
2819impl RepairFootprint {
2820 pub fn new(
2822 session_id: impl Into<String>,
2823 node_id: impl Into<String>,
2824 revision_id: impl Into<String>,
2825 attempt: u32,
2826 bundle: &ArtifactBundle,
2827 diagnosis: impl Into<String>,
2828 ) -> Self {
2829 let affected_files = bundle
2830 .affected_paths()
2831 .into_iter()
2832 .map(String::from)
2833 .collect();
2834 Self {
2835 footprint_id: uuid_v4(),
2836 session_id: session_id.into(),
2837 node_id: node_id.into(),
2838 revision_id: revision_id.into(),
2839 attempt,
2840 affected_files,
2841 applied_bundle: bundle.clone(),
2842 diagnosis: diagnosis.into(),
2843 resolved: false,
2844 created_at: epoch_secs(),
2845 }
2846 }
2847
2848 pub fn mark_resolved(&mut self) {
2850 self.resolved = true;
2851 }
2852}
2853
2854#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2860pub struct DependencyExpectation {
2861 pub required_packages: Vec<String>,
2863 pub setup_commands: Vec<String>,
2865 pub min_toolchain_version: Option<String>,
2867}
2868
2869#[derive(Debug, Clone, Serialize, Deserialize)]
2874pub struct BudgetEnvelope {
2875 pub session_id: String,
2877 pub max_steps: Option<u32>,
2879 pub steps_used: u32,
2881 pub max_revisions: Option<u32>,
2883 pub revisions_used: u32,
2885 pub max_cost_usd: Option<f64>,
2887 pub cost_used_usd: f64,
2889}
2890
2891impl BudgetEnvelope {
2892 pub fn new(session_id: impl Into<String>) -> Self {
2894 Self {
2895 session_id: session_id.into(),
2896 max_steps: None,
2897 steps_used: 0,
2898 max_revisions: None,
2899 revisions_used: 0,
2900 max_cost_usd: None,
2901 cost_used_usd: 0.0,
2902 }
2903 }
2904
2905 pub fn steps_exhausted(&self) -> bool {
2907 self.max_steps.is_some_and(|max| self.steps_used >= max)
2908 }
2909
2910 pub fn revisions_exhausted(&self) -> bool {
2912 self.max_revisions
2913 .is_some_and(|max| self.revisions_used >= max)
2914 }
2915
2916 pub fn cost_exhausted(&self) -> bool {
2918 self.max_cost_usd
2919 .is_some_and(|max| self.cost_used_usd >= max)
2920 }
2921
2922 pub fn any_exhausted(&self) -> bool {
2924 self.steps_exhausted() || self.revisions_exhausted() || self.cost_exhausted()
2925 }
2926
2927 pub fn record_step(&mut self) {
2929 self.steps_used += 1;
2930 }
2931
2932 pub fn record_revision(&mut self) {
2934 self.revisions_used += 1;
2935 }
2936
2937 pub fn record_cost(&mut self, usd: f64) {
2939 self.cost_used_usd += usd;
2940 }
2941}
2942
2943fn epoch_secs() -> i64 {
2945 std::time::SystemTime::now()
2946 .duration_since(std::time::UNIX_EPOCH)
2947 .unwrap_or_default()
2948 .as_secs() as i64
2949}
2950
2951fn uuid_v4() -> String {
2953 use std::collections::hash_map::DefaultHasher;
2956 use std::hash::{Hash, Hasher};
2957 let now = std::time::SystemTime::now()
2958 .duration_since(std::time::UNIX_EPOCH)
2959 .unwrap_or_default();
2960 let mut hasher = DefaultHasher::new();
2961 now.as_nanos().hash(&mut hasher);
2962 format!("{:016x}", hasher.finish())
2963}
2964
2965#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2974#[serde(rename_all = "snake_case")]
2975pub enum ParseResultState {
2976 StrictJsonOk,
2978 TolerantRecoveryOk,
2980 NoStructuredPayload,
2982 SchemaInvalid,
2984 SemanticallyRejected,
2987 EmptyBundle,
2989}
2990
2991impl ParseResultState {
2992 pub fn is_ok(&self) -> bool {
2994 matches!(self, Self::StrictJsonOk | Self::TolerantRecoveryOk)
2995 }
2996}
2997
2998impl std::fmt::Display for ParseResultState {
2999 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3000 match self {
3001 Self::StrictJsonOk => write!(f, "strict_json_ok"),
3002 Self::TolerantRecoveryOk => write!(f, "tolerant_recovery_ok"),
3003 Self::NoStructuredPayload => write!(f, "no_structured_payload"),
3004 Self::SchemaInvalid => write!(f, "schema_invalid"),
3005 Self::SemanticallyRejected => write!(f, "semantically_rejected"),
3006 Self::EmptyBundle => write!(f, "empty_bundle"),
3007 }
3008 }
3009}
3010
3011#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3016#[serde(rename_all = "snake_case")]
3017pub enum RetryClassification {
3018 MalformedRetry,
3020 Retarget,
3022 SupportFileViolation,
3024 Replan,
3026 BudgetExhausted,
3028}
3029
3030impl std::fmt::Display for RetryClassification {
3031 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3032 match self {
3033 Self::MalformedRetry => write!(f, "malformed_retry"),
3034 Self::Retarget => write!(f, "retarget"),
3035 Self::SupportFileViolation => write!(f, "support_file_violation"),
3036 Self::Replan => write!(f, "replan"),
3037 Self::BudgetExhausted => write!(f, "budget_exhausted"),
3038 }
3039 }
3040}
3041
3042#[derive(Debug, Clone, Serialize, Deserialize)]
3047pub struct CorrectionAttemptRecord {
3048 pub attempt: u32,
3050 pub parse_state: ParseResultState,
3052 pub retry_classification: Option<RetryClassification>,
3054 pub response_fingerprint: String,
3056 pub response_length: usize,
3058 pub energy_after: Option<EnergyComponents>,
3060 pub accepted: bool,
3062 pub rejection_reason: Option<String>,
3064 pub created_at: i64,
3066}
3067
3068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3073#[serde(rename_all = "snake_case")]
3074pub enum PromptIntent {
3075 ArchitectExisting,
3077 ArchitectGreenfield,
3079 ActuatorMultiOutput,
3081 ActuatorSingleOutput,
3083 VerifierAnalysis,
3085 CorrectionRetry,
3087 BundleRetarget,
3089 SpeculatorBasic,
3091 SpeculatorLookahead,
3093 SoloGenerate,
3095 SoloCorrect,
3097 ProjectNameSuggest,
3099}
3100
3101#[derive(Debug, Clone, Serialize, Deserialize)]
3106pub struct PromptProvenance {
3107 pub intent: PromptIntent,
3109 pub plugin_fragment_source: Option<String>,
3111 pub evidence_sources: Vec<String>,
3113 pub compiled_at: i64,
3115}
3116
3117#[derive(Debug, Clone, Serialize, Deserialize)]
3122pub struct CompiledPrompt {
3123 pub text: String,
3125 pub provenance: PromptProvenance,
3127}
3128
3129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3134pub struct PromptEvidence {
3135 pub user_goal: Option<String>,
3137 pub project_summary: Option<String>,
3139 pub node_goal: Option<String>,
3141 pub output_files: Vec<String>,
3143 pub context_files: Vec<String>,
3145 pub verifier_diagnostics: Option<String>,
3147 pub previous_attempts: Vec<CorrectionAttemptRecord>,
3149 pub previous_attempt_count: usize,
3151 pub plugin_correction_fragment: Option<String>,
3153 pub legal_support_files: Vec<String>,
3155 pub existing_file_contents: Vec<(String, String)>,
3157 pub dependency_expectations: Option<DependencyExpectation>,
3159 pub rejected_bundle_summary: Option<String>,
3161 pub solo_file_path: Option<String>,
3163 pub solo_language: Option<String>,
3165 pub working_dir: Option<String>,
3167 pub active_plugins: Vec<String>,
3169 pub interface_signature: Option<String>,
3171 pub invariants: Option<String>,
3173 pub forbidden_patterns: Option<String>,
3175 pub weighted_tests: Option<String>,
3177 pub workspace_import_hints: Option<String>,
3179 pub evidence_section: Option<String>,
3181 pub error_feedback: Option<String>,
3183 pub restriction_map_context: Option<String>,
3185 pub project_file_tree: Option<String>,
3187 pub build_test_output: Option<String>,
3189 pub owner_plugin: Option<String>,
3191 pub energy_v_syn: Option<f32>,
3193}
3194
3195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3197pub enum CommandPolicyDecision {
3198 Allow,
3200 Deny,
3202 RequireApproval,
3204}
3205
3206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3208pub enum ManifestMutationPolicy {
3209 Allow,
3211 Deny,
3213}
3214
3215#[cfg(test)]
3216mod psp5_tests {
3217 use super::*;
3218
3219 #[test]
3220 fn test_execution_mode_default_is_project() {
3221 assert_eq!(ExecutionMode::default(), ExecutionMode::Project);
3222 }
3223
3224 #[test]
3225 fn test_node_class_default_is_implementation() {
3226 assert_eq!(NodeClass::default(), NodeClass::Implementation);
3227 }
3228
3229 #[test]
3230 fn test_artifact_bundle_roundtrip() {
3231 let bundle = ArtifactBundle {
3232 artifacts: vec![
3233 ArtifactOperation::Write {
3234 path: "src/main.rs".to_string(),
3235 content: "fn main() {}".to_string(),
3236 },
3237 ArtifactOperation::Diff {
3238 path: "src/lib.rs".to_string(),
3239 patch: "--- a\n+++ b\n@@ -1 +1 @@\n-old\n+new".to_string(),
3240 },
3241 ],
3242 commands: vec!["cargo build".to_string()],
3243 };
3244
3245 let json = serde_json::to_string(&bundle).unwrap();
3246 let deser: ArtifactBundle = serde_json::from_str(&json).unwrap();
3247
3248 assert_eq!(deser.len(), 2);
3249 assert_eq!(deser.writes_count(), 1);
3250 assert_eq!(deser.diffs_count(), 1);
3251 assert_eq!(deser.commands.len(), 1);
3252 }
3253
3254 #[test]
3255 fn test_artifact_bundle_validate_empty() {
3256 let bundle = ArtifactBundle::new();
3257 assert!(bundle.validate().is_err());
3258 }
3259
3260 #[test]
3261 fn test_artifact_bundle_validate_absolute_path() {
3262 let bundle = ArtifactBundle {
3263 artifacts: vec![ArtifactOperation::Write {
3264 path: "/etc/passwd".to_string(),
3265 content: "bad".to_string(),
3266 }],
3267 commands: vec![],
3268 };
3269 assert!(bundle.validate().is_err());
3270 assert!(bundle.validate().unwrap_err().contains("absolute path"));
3271 }
3272
3273 #[test]
3274 fn test_artifact_bundle_validate_path_traversal() {
3275 let bundle = ArtifactBundle {
3276 artifacts: vec![ArtifactOperation::Write {
3277 path: "../../etc/passwd".to_string(),
3278 content: "bad".to_string(),
3279 }],
3280 commands: vec![],
3281 };
3282 assert!(bundle.validate().is_err());
3283 assert!(bundle.validate().unwrap_err().contains("path traversal"));
3284 }
3285
3286 #[test]
3287 fn test_artifact_bundle_validate_ok() {
3288 let bundle = ArtifactBundle {
3289 artifacts: vec![ArtifactOperation::Write {
3290 path: "src/main.rs".to_string(),
3291 content: "fn main() {}".to_string(),
3292 }],
3293 commands: vec![],
3294 };
3295 assert!(bundle.validate().is_ok());
3296 }
3297
3298 #[test]
3299 fn test_artifact_operation_accessors() {
3300 let write = ArtifactOperation::Write {
3301 path: "foo.rs".to_string(),
3302 content: "bar".to_string(),
3303 };
3304 assert_eq!(write.path(), "foo.rs");
3305 assert!(write.is_write());
3306 assert!(!write.is_diff());
3307
3308 let diff = ArtifactOperation::Diff {
3309 path: "baz.rs".to_string(),
3310 patch: "patch".to_string(),
3311 };
3312 assert_eq!(diff.path(), "baz.rs");
3313 assert!(!diff.is_write());
3314 assert!(diff.is_diff());
3315 }
3316
3317 #[test]
3318 fn test_affected_paths_deduplication() {
3319 let bundle = ArtifactBundle {
3320 artifacts: vec![
3321 ArtifactOperation::Write {
3322 path: "src/main.rs".to_string(),
3323 content: "v1".to_string(),
3324 },
3325 ArtifactOperation::Diff {
3326 path: "src/main.rs".to_string(),
3327 patch: "patch".to_string(),
3328 },
3329 ],
3330 commands: vec![],
3331 };
3332 assert_eq!(bundle.affected_paths().len(), 1);
3333 }
3334
3335 #[test]
3336 fn test_verification_result_all_passed() {
3337 let mut result = VerificationResult::default();
3338 assert!(!result.all_passed()); result.syntax_ok = true;
3341 result.build_ok = true;
3342 result.tests_ok = true;
3343 assert!(result.all_passed());
3344 }
3345
3346 #[test]
3347 fn test_verification_result_degraded() {
3348 let result = VerificationResult::degraded("no cargo");
3349 assert!(result.degraded);
3350 assert!(!result.all_passed());
3351 assert_eq!(result.degraded_reason.unwrap(), "no cargo");
3352 }
3353
3354 #[test]
3359 fn test_ownership_manifest_assign_and_lookup() {
3360 let mut manifest = OwnershipManifest::new();
3361 manifest.assign("src/main.rs", "node_1", "rust", NodeClass::Implementation);
3362 manifest.assign("src/lib.rs", "node_1", "rust", NodeClass::Implementation);
3363 manifest.assign("tests/test.rs", "node_2", "rust", NodeClass::Integration);
3364
3365 let entry = manifest.owner_of("src/main.rs").unwrap();
3367 assert_eq!(entry.owner_node_id, "node_1");
3368 assert_eq!(entry.owner_plugin, "rust");
3369 assert_eq!(entry.node_class, NodeClass::Implementation);
3370
3371 assert!(manifest.owner_of("nonexistent.rs").is_none());
3372
3373 let mut files = manifest.files_owned_by("node_1");
3375 files.sort();
3376 assert_eq!(files, vec!["src/lib.rs", "src/main.rs"]);
3377
3378 let files_2 = manifest.files_owned_by("node_2");
3379 assert_eq!(files_2, vec!["tests/test.rs"]);
3380
3381 assert_eq!(manifest.len(), 3);
3382 assert!(!manifest.is_empty());
3383 }
3384
3385 #[test]
3386 fn test_ownership_manifest_validate_bundle_ok() {
3387 let mut manifest = OwnershipManifest::new();
3388 manifest.assign("src/main.rs", "node_1", "rust", NodeClass::Implementation);
3389 manifest.assign("src/lib.rs", "node_1", "rust", NodeClass::Implementation);
3390
3391 let bundle = ArtifactBundle {
3392 artifacts: vec![
3393 ArtifactOperation::Write {
3394 path: "src/main.rs".to_string(),
3395 content: "fn main() {}".to_string(),
3396 },
3397 ArtifactOperation::Write {
3398 path: "src/lib.rs".to_string(),
3399 content: "pub fn lib() {}".to_string(),
3400 },
3401 ],
3402 commands: vec![],
3403 };
3404
3405 assert!(manifest
3407 .validate_bundle(&bundle, "node_1", NodeClass::Implementation)
3408 .is_ok());
3409 }
3410
3411 #[test]
3412 fn test_ownership_manifest_validate_bundle_cross_owner_rejected() {
3413 let mut manifest = OwnershipManifest::new();
3414 manifest.assign("src/main.rs", "node_1", "rust", NodeClass::Implementation);
3415 manifest.assign("src/other.rs", "node_2", "rust", NodeClass::Implementation);
3416
3417 let bundle = ArtifactBundle {
3418 artifacts: vec![
3419 ArtifactOperation::Write {
3420 path: "src/main.rs".to_string(),
3421 content: "fn main() {}".to_string(),
3422 },
3423 ArtifactOperation::Write {
3424 path: "src/other.rs".to_string(),
3425 content: "fn other() {}".to_string(),
3426 },
3427 ],
3428 commands: vec![],
3429 };
3430
3431 let result = manifest.validate_bundle(&bundle, "node_1", NodeClass::Implementation);
3433 assert!(result.is_err());
3434 assert!(result.unwrap_err().contains("Ownership violation"));
3435 }
3436
3437 #[test]
3438 fn test_ownership_manifest_validate_integration_cross_owner_ok() {
3439 let mut manifest = OwnershipManifest::new();
3440 manifest.assign("src/main.rs", "node_1", "rust", NodeClass::Implementation);
3441 manifest.assign("src/other.rs", "node_2", "rust", NodeClass::Implementation);
3442
3443 let bundle = ArtifactBundle {
3444 artifacts: vec![
3445 ArtifactOperation::Write {
3446 path: "src/main.rs".to_string(),
3447 content: "fn main() {}".to_string(),
3448 },
3449 ArtifactOperation::Write {
3450 path: "src/other.rs".to_string(),
3451 content: "fn other() {}".to_string(),
3452 },
3453 ],
3454 commands: vec![],
3455 };
3456
3457 let result = manifest.validate_bundle(&bundle, "node_3", NodeClass::Integration);
3459 assert!(result.is_ok());
3460 }
3461
3462 #[test]
3463 fn test_ownership_manifest_fanout_limit() {
3464 let manifest = OwnershipManifest::with_fanout_limit(2);
3465
3466 let bundle = ArtifactBundle {
3467 artifacts: vec![
3468 ArtifactOperation::Write {
3469 path: "a.rs".to_string(),
3470 content: "a".to_string(),
3471 },
3472 ArtifactOperation::Write {
3473 path: "b.rs".to_string(),
3474 content: "b".to_string(),
3475 },
3476 ArtifactOperation::Write {
3477 path: "c.rs".to_string(),
3478 content: "c".to_string(),
3479 },
3480 ],
3481 commands: vec![],
3482 };
3483
3484 let result = manifest.validate_bundle(&bundle, "node_1", NodeClass::Implementation);
3486 assert!(result.is_err());
3487 assert!(result.unwrap_err().contains("fanout limit"));
3488
3489 let small_bundle = ArtifactBundle {
3491 artifacts: vec![
3492 ArtifactOperation::Write {
3493 path: "a.rs".to_string(),
3494 content: "a".to_string(),
3495 },
3496 ArtifactOperation::Write {
3497 path: "b.rs".to_string(),
3498 content: "b".to_string(),
3499 },
3500 ],
3501 commands: vec![],
3502 };
3503 assert!(manifest
3504 .validate_bundle(&small_bundle, "node_1", NodeClass::Implementation)
3505 .is_ok());
3506 }
3507
3508 #[test]
3509 fn test_ownership_manifest_assign_new_paths() {
3510 let mut manifest = OwnershipManifest::new();
3511 manifest.assign("src/main.rs", "node_1", "rust", NodeClass::Implementation);
3512
3513 let bundle = ArtifactBundle {
3514 artifacts: vec![
3515 ArtifactOperation::Write {
3516 path: "src/main.rs".to_string(),
3517 content: "existing".to_string(),
3518 },
3519 ArtifactOperation::Write {
3520 path: "src/new_file.rs".to_string(),
3521 content: "new".to_string(),
3522 },
3523 ],
3524 commands: vec![],
3525 };
3526
3527 manifest.assign_new_paths(&bundle, "node_1", "rust", NodeClass::Implementation);
3528
3529 assert_eq!(
3531 manifest.owner_of("src/main.rs").unwrap().owner_node_id,
3532 "node_1"
3533 );
3534 let new_entry = manifest.owner_of("src/new_file.rs").unwrap();
3536 assert_eq!(new_entry.owner_node_id, "node_1");
3537 assert_eq!(new_entry.owner_plugin, "rust");
3538 assert_eq!(manifest.len(), 2);
3539 }
3540
3541 #[test]
3546 fn test_plan_validate_duplicate_output_files_rejected() {
3547 let plan = TaskPlan {
3548 tasks: vec![
3549 PlannedTask {
3550 id: "task_1".into(),
3551 goal: "Create math module".into(),
3552 output_files: vec!["src/math.py".into(), "tests/test_math.py".into()],
3553 ..PlannedTask::new("task_1", "Create math module")
3554 },
3555 PlannedTask {
3556 id: "task_2".into(),
3557 goal: "Create tests".into(),
3558 output_files: vec!["tests/test_math.py".into()],
3559 ..PlannedTask::new("task_2", "Create tests")
3560 },
3561 ],
3562 };
3563 let result = plan.validate();
3564 assert!(result.is_err());
3565 let err = result.unwrap_err();
3566 assert!(
3567 err.contains("tests/test_math.py"),
3568 "Error should mention the duplicate file: {}",
3569 err
3570 );
3571 assert!(
3572 err.contains("Ownership violation"),
3573 "Error should mention ownership: {}",
3574 err
3575 );
3576 }
3577
3578 #[test]
3579 fn test_plan_validate_unique_output_files_ok() {
3580 let plan = TaskPlan {
3581 tasks: vec![
3582 PlannedTask {
3583 id: "task_1".into(),
3584 goal: "Create math module".into(),
3585 output_files: vec!["src/math.py".into()],
3586 ..PlannedTask::new("task_1", "Create math module")
3587 },
3588 PlannedTask {
3589 id: "test_1".into(),
3590 goal: "Tests for math".into(),
3591 output_files: vec!["tests/test_math.py".into()],
3592 dependencies: vec!["task_1".into()],
3593 ..PlannedTask::new("test_1", "Tests for math")
3594 },
3595 ],
3596 };
3597 assert!(plan.validate().is_ok());
3598 }
3599
3600 #[test]
3601 fn test_plan_validate_context_files_do_not_conflict_with_output_files() {
3602 let plan = TaskPlan {
3604 tasks: vec![
3605 PlannedTask {
3606 id: "task_1".into(),
3607 goal: "Create math module".into(),
3608 output_files: vec!["src/math.py".into()],
3609 ..PlannedTask::new("task_1", "Create math module")
3610 },
3611 PlannedTask {
3612 id: "test_1".into(),
3613 goal: "Tests for math".into(),
3614 context_files: vec!["src/math.py".into()], output_files: vec!["tests/test_math.py".into()],
3616 dependencies: vec!["task_1".into()],
3617 ..PlannedTask::new("test_1", "Tests for math")
3618 },
3619 ],
3620 };
3621 assert!(plan.validate().is_ok());
3622 }
3623
3624 #[test]
3629 fn test_structural_digest_from_content() {
3630 let digest = StructuralDigest::from_content(
3631 "node_1",
3632 "src/main.rs",
3633 ArtifactKind::Signature,
3634 b"fn main() {}",
3635 );
3636
3637 assert_eq!(digest.source_node_id, "node_1");
3638 assert_eq!(digest.source_path, "src/main.rs");
3639 assert_eq!(digest.artifact_kind, ArtifactKind::Signature);
3640 assert_eq!(digest.version, 1);
3641 assert!(!digest.digest_id.is_empty());
3642 assert_ne!(digest.hash, [0u8; 32]);
3644 }
3645
3646 #[test]
3647 fn test_structural_digest_matches() {
3648 let d1 = StructuralDigest::from_content(
3649 "node_1",
3650 "src/main.rs",
3651 ArtifactKind::Signature,
3652 b"fn main() {}",
3653 );
3654 let d2 = StructuralDigest::from_content(
3655 "node_1",
3656 "src/main.rs",
3657 ArtifactKind::Signature,
3658 b"fn main() {}",
3659 );
3660 let d3 = StructuralDigest::from_content(
3661 "node_1",
3662 "src/main.rs",
3663 ArtifactKind::Signature,
3664 b"fn main() { println!(); }",
3665 );
3666
3667 assert!(d1.matches(&d2));
3668 assert!(!d1.matches(&d3));
3669 }
3670
3671 #[test]
3672 fn test_context_budget_default() {
3673 let budget = ContextBudget::default();
3674 assert_eq!(budget.byte_limit, 100 * 1024); assert_eq!(budget.file_count_limit, 20);
3676 }
3677
3678 #[test]
3679 fn test_restriction_map_for_node() {
3680 let map = RestrictionMap::for_node("node_1".to_string());
3681 assert_eq!(map.node_id, "node_1");
3682 assert!(map.owned_files.is_empty());
3683 assert!(map.sealed_interfaces.is_empty());
3684 assert_eq!(map.budget, ContextBudget::default());
3685 }
3686
3687 #[test]
3688 fn test_restriction_map_structural_bytes() {
3689 let mut map = RestrictionMap::for_node("node_1".to_string());
3690 let d = StructuralDigest::from_content(
3691 "n1",
3692 "src/a.rs",
3693 ArtifactKind::InterfaceSeal,
3694 b"content",
3695 );
3696 map.structural_digests.push(d);
3697 assert!(map.structural_bytes() > 0);
3699 }
3700
3701 #[test]
3702 fn test_context_package_add_file_within_budget() {
3703 let mut pkg = ContextPackage::new("node_1".to_string());
3704 pkg.restriction_map.budget.byte_limit = 1024;
3705
3706 assert!(pkg.add_file("a.rs", "hello world".to_string()));
3707 assert_eq!(pkg.included_files.len(), 1);
3708 assert_eq!(pkg.total_bytes, 11);
3709 assert!(!pkg.budget_exceeded);
3710 }
3711
3712 #[test]
3713 fn test_context_package_add_file_exceeds_budget() {
3714 let mut pkg = ContextPackage::new("node_1".to_string());
3715 pkg.restriction_map.budget.byte_limit = 10;
3716
3717 let result = pkg.add_file("big.rs", "this is more than ten bytes".to_string());
3718 assert!(!result);
3719 assert!(pkg.budget_exceeded);
3720 assert!(pkg.included_files.is_empty());
3722 }
3723
3724 #[test]
3725 fn test_context_package_provenance() {
3726 let mut pkg = ContextPackage::new("node_1".to_string());
3727 pkg.add_file("a.rs", "content".to_string());
3728
3729 let d = StructuralDigest::from_content("n1", "src/a.rs", ArtifactKind::Signature, b"data");
3730 pkg.add_structural_digest(d);
3731
3732 let prov = pkg.provenance();
3733 assert_eq!(prov.node_id, "node_1");
3734 assert_eq!(prov.context_package_id, pkg.package_id);
3735 assert_eq!(prov.included_file_count, 1);
3736 assert_eq!(prov.structural_digest_hashes.len(), 1);
3737 assert!(prov.total_bytes > 0);
3738 }
3739
3740 #[test]
3741 fn test_context_provenance_default() {
3742 let prov = ContextProvenance::default();
3743 assert!(prov.node_id.is_empty());
3744 assert!(prov.structural_digest_hashes.is_empty());
3745 assert_eq!(prov.included_file_count, 0);
3746 }
3747
3748 #[test]
3749 fn test_artifact_kind_display() {
3750 assert_eq!(format!("{}", ArtifactKind::Signature), "signature");
3751 assert_eq!(format!("{}", ArtifactKind::InterfaceSeal), "interface_seal");
3752 }
3753
3754 #[test]
3755 fn test_sensor_status_display() {
3756 assert_eq!(format!("{}", SensorStatus::Available), "available");
3757 assert_eq!(
3758 format!(
3759 "{}",
3760 SensorStatus::Fallback {
3761 actual: "ruff".into(),
3762 reason: "primary not found".into()
3763 }
3764 ),
3765 "fallback(ruff)"
3766 );
3767 assert_eq!(
3768 format!(
3769 "{}",
3770 SensorStatus::Unavailable {
3771 reason: "not installed".into()
3772 }
3773 ),
3774 "unavailable(not installed)"
3775 );
3776 }
3777
3778 #[test]
3779 fn test_verification_result_no_degraded_stages() {
3780 let result = VerificationResult {
3781 syntax_ok: true,
3782 build_ok: true,
3783 tests_ok: true,
3784 lint_ok: true,
3785 stage_outcomes: vec![StageOutcome {
3786 stage: "syntax_check".into(),
3787 passed: true,
3788 sensor_status: SensorStatus::Available,
3789 output: None,
3790 }],
3791 ..Default::default()
3792 };
3793 assert!(result.all_passed());
3794 assert!(!result.has_degraded_stages());
3795 assert!(result.degraded_stage_reasons().is_empty());
3796 }
3797
3798 #[test]
3799 fn test_verification_result_with_fallback_blocks_stability() {
3800 let result = VerificationResult {
3801 syntax_ok: true,
3802 build_ok: true,
3803 tests_ok: true,
3804 lint_ok: true,
3805 stage_outcomes: vec![
3806 StageOutcome {
3807 stage: "syntax_check".into(),
3808 passed: true,
3809 sensor_status: SensorStatus::Available,
3810 output: None,
3811 },
3812 StageOutcome {
3813 stage: "test".into(),
3814 passed: true,
3815 sensor_status: SensorStatus::Fallback {
3816 actual: "python -m pytest".into(),
3817 reason: "uv not found".into(),
3818 },
3819 output: None,
3820 },
3821 ],
3822 ..Default::default()
3823 };
3824 assert!(result.has_degraded_stages());
3826 let reasons = result.degraded_stage_reasons();
3827 assert_eq!(reasons.len(), 1);
3828 assert!(reasons[0].contains("test"));
3829 assert!(reasons[0].contains("fallback"));
3830 }
3831
3832 #[test]
3833 fn test_verification_result_unavailable_stage() {
3834 let result = VerificationResult {
3835 syntax_ok: false,
3836 stage_outcomes: vec![StageOutcome {
3837 stage: "lint".into(),
3838 passed: false,
3839 sensor_status: SensorStatus::Unavailable {
3840 reason: "clippy not installed".into(),
3841 },
3842 output: None,
3843 }],
3844 ..Default::default()
3845 };
3846 assert!(result.has_degraded_stages());
3847 let reasons = result.degraded_stage_reasons();
3848 assert!(reasons[0].contains("clippy not installed"));
3849 }
3850
3851 #[test]
3852 fn test_verification_result_mixed_stages() {
3853 let result = VerificationResult {
3855 syntax_ok: true,
3856 tests_ok: false,
3857 lint_ok: false,
3858 stage_outcomes: vec![
3859 StageOutcome {
3860 stage: "syntax_check".into(),
3861 passed: true,
3862 sensor_status: SensorStatus::Available,
3863 output: Some("OK".into()),
3864 },
3865 StageOutcome {
3866 stage: "lint".into(),
3867 passed: true,
3868 sensor_status: SensorStatus::Fallback {
3869 actual: "cargo check".into(),
3870 reason: "clippy not found".into(),
3871 },
3872 output: Some("warnings only".into()),
3873 },
3874 StageOutcome {
3875 stage: "test".into(),
3876 passed: false,
3877 sensor_status: SensorStatus::Unavailable {
3878 reason: "no test runner".into(),
3879 },
3880 output: None,
3881 },
3882 ],
3883 ..Default::default()
3884 };
3885 assert!(result.has_degraded_stages());
3886 let reasons = result.degraded_stage_reasons();
3887 assert_eq!(reasons.len(), 2);
3889 assert!(reasons.iter().any(|r| r.contains("lint")));
3890 assert!(reasons.iter().any(|r| r.contains("test")));
3891 }
3892
3893 #[test]
3898 fn test_escalation_category_display() {
3899 assert_eq!(
3900 EscalationCategory::ImplementationError.to_string(),
3901 "implementation_error"
3902 );
3903 assert_eq!(
3904 EscalationCategory::ContractMismatch.to_string(),
3905 "contract_mismatch"
3906 );
3907 assert_eq!(
3908 EscalationCategory::DegradedSensors.to_string(),
3909 "degraded_sensors"
3910 );
3911 }
3912
3913 #[test]
3914 fn test_rewrite_action_grounded_retry() {
3915 let action = RewriteAction::GroundedRetry {
3916 evidence_summary: "build failed twice".into(),
3917 };
3918 match action {
3919 RewriteAction::GroundedRetry { evidence_summary } => {
3920 assert!(evidence_summary.contains("build failed"));
3921 }
3922 _ => panic!("Expected GroundedRetry"),
3923 }
3924 }
3925
3926 #[test]
3927 fn test_rewrite_action_node_split() {
3928 let action = RewriteAction::NodeSplit {
3929 proposed_children: vec!["child_a".into(), "child_b".into()],
3930 };
3931 match action {
3932 RewriteAction::NodeSplit { proposed_children } => {
3933 assert_eq!(proposed_children.len(), 2);
3934 }
3935 _ => panic!("Expected NodeSplit"),
3936 }
3937 }
3938
3939 #[test]
3940 fn test_sheaf_validator_class_display() {
3941 assert_eq!(
3942 SheafValidatorClass::DependencyGraphConsistency.to_string(),
3943 "dependency_graph"
3944 );
3945 assert_eq!(
3946 SheafValidatorClass::CrossLanguageBoundary.to_string(),
3947 "cross_language"
3948 );
3949 }
3950
3951 #[test]
3952 fn test_sheaf_validation_result_passed() {
3953 let result = SheafValidationResult::passed(
3954 SheafValidatorClass::DependencyGraphConsistency,
3955 vec!["node_1".into()],
3956 );
3957 assert!(result.passed);
3958 assert_eq!(result.v_sheaf_contribution, 0.0);
3959 assert!(result.evidence_summary.is_empty());
3960 assert!(result.requeue_targets.is_empty());
3961 }
3962
3963 #[test]
3964 fn test_sheaf_validation_result_failed() {
3965 let result = SheafValidationResult::failed(
3966 SheafValidatorClass::ExportImportConsistency,
3967 "ownership mismatch on 2 files",
3968 vec!["src/a.rs".into(), "src/b.rs".into()],
3969 vec!["node_2".into()],
3970 0.3,
3971 );
3972 assert!(!result.passed);
3973 assert_eq!(result.v_sheaf_contribution, 0.3);
3974 assert!(result.evidence_summary.contains("ownership mismatch"));
3975 assert_eq!(result.affected_files.len(), 2);
3976 assert_eq!(result.requeue_targets, vec!["node_2"]);
3977 }
3978
3979 #[test]
3980 fn test_escalation_report_roundtrip() {
3981 let report = EscalationReport {
3982 node_id: "test_node".into(),
3983 session_id: "sess_1".into(),
3984 category: EscalationCategory::TopologyMismatch,
3985 action: RewriteAction::InterfaceInsertion {
3986 boundary: "module_boundary".into(),
3987 },
3988 energy_snapshot: EnergyComponents::default(),
3989 stage_outcomes: Vec::new(),
3990 evidence: "violation at boundary".into(),
3991 affected_node_ids: vec!["dep_1".into()],
3992 timestamp: 12345,
3993 };
3994 let json = serde_json::to_string(&report).unwrap();
3995 let deser: EscalationReport = serde_json::from_str(&json).unwrap();
3996 assert_eq!(deser.node_id, "test_node");
3997 assert_eq!(deser.category, EscalationCategory::TopologyMismatch);
3998 assert_eq!(deser.affected_node_ids.len(), 1);
3999 }
4000
4001 #[test]
4002 fn test_stability_monitor_reset_for_replan() {
4003 let mut monitor = StabilityMonitor::new();
4004 monitor.record_energy(0.8);
4005 monitor.record_energy(0.5);
4006 monitor.record_failure(ErrorType::Compilation);
4007 assert_eq!(monitor.attempt_count, 2);
4008
4009 monitor.reset_for_replan();
4010 assert_eq!(monitor.attempt_count, 0);
4011 assert!(!monitor.stable);
4012 assert_eq!(monitor.energy_history.len(), 2);
4014 }
4015
4016 #[test]
4017 fn test_rewrite_record_serialization() {
4018 let record = RewriteRecord {
4019 node_id: "n1".into(),
4020 session_id: "s1".into(),
4021 action: RewriteAction::SubgraphReplan {
4022 affected_nodes: vec!["n2".into(), "n3".into()],
4023 },
4024 category: EscalationCategory::InsufficientModelCapability,
4025 requeued_nodes: vec!["n2".into(), "n3".into()],
4026 inserted_nodes: Vec::new(),
4027 timestamp: 99999,
4028 };
4029 let json = serde_json::to_string(&record).unwrap();
4030 let deser: RewriteRecord = serde_json::from_str(&json).unwrap();
4031 assert_eq!(deser.requeued_nodes.len(), 2);
4032 assert!(deser.inserted_nodes.is_empty());
4033 }
4034
4035 #[test]
4040 fn test_provisional_branch_state_display() {
4041 assert_eq!(ProvisionalBranchState::Active.to_string(), "active");
4042 assert_eq!(ProvisionalBranchState::Sealed.to_string(), "sealed");
4043 assert_eq!(ProvisionalBranchState::Merged.to_string(), "merged");
4044 assert_eq!(ProvisionalBranchState::Flushed.to_string(), "flushed");
4045 }
4046
4047 #[test]
4048 fn test_provisional_branch_lifecycle() {
4049 let branch = ProvisionalBranch::new("b1", "s1", "node_child", "node_parent");
4050 assert_eq!(branch.state, ProvisionalBranchState::Active);
4051 assert!(branch.is_live());
4052 assert!(!branch.is_flushed());
4053 assert!(branch.parent_seal_hash.is_none());
4054 assert!(branch.sandbox_dir.is_none());
4055 assert!(branch.created_at > 0);
4056 }
4057
4058 #[test]
4059 fn test_provisional_branch_flushed_not_live() {
4060 let mut branch = ProvisionalBranch::new("b1", "s1", "n1", "p1");
4061 branch.state = ProvisionalBranchState::Flushed;
4062 assert!(!branch.is_live());
4063 assert!(branch.is_flushed());
4064 }
4065
4066 #[test]
4067 fn test_provisional_branch_sealed_is_live() {
4068 let mut branch = ProvisionalBranch::new("b1", "s1", "n1", "p1");
4069 branch.state = ProvisionalBranchState::Sealed;
4070 assert!(branch.is_live());
4071 assert!(!branch.is_flushed());
4072 }
4073
4074 #[test]
4075 fn test_provisional_branch_serialization() {
4076 let branch = ProvisionalBranch::new("b1", "s1", "n1", "p1");
4077 let json = serde_json::to_string(&branch).unwrap();
4078 let deser: ProvisionalBranch = serde_json::from_str(&json).unwrap();
4079 assert_eq!(deser.branch_id, "b1");
4080 assert_eq!(deser.state, ProvisionalBranchState::Active);
4081 }
4082
4083 #[test]
4084 fn test_branch_lineage_serialization() {
4085 let lineage = BranchLineage {
4086 lineage_id: "lin_1".into(),
4087 parent_branch_id: "parent_b".into(),
4088 child_branch_id: "child_b".into(),
4089 depends_on_seal: true,
4090 };
4091 let json = serde_json::to_string(&lineage).unwrap();
4092 let deser: BranchLineage = serde_json::from_str(&json).unwrap();
4093 assert!(deser.depends_on_seal);
4094 assert_eq!(deser.parent_branch_id, "parent_b");
4095 }
4096
4097 #[test]
4098 fn test_interface_seal_from_digest() {
4099 let digest = StructuralDigest::from_content(
4100 "node_iface",
4101 "src/api.rs",
4102 ArtifactKind::InterfaceSeal,
4103 b"pub fn hello() -> String",
4104 );
4105 let seal = InterfaceSealRecord::from_digest("sess1", "node_iface", &digest);
4106 assert_eq!(seal.node_id, "node_iface");
4107 assert_eq!(seal.sealed_path, "src/api.rs");
4108 assert!(seal.matches_hash(&digest.hash));
4109 assert!(!seal.matches_hash(&[0u8; 32]));
4110 }
4111
4112 #[test]
4113 fn test_branch_flush_record() {
4114 let flush = BranchFlushRecord::new(
4115 "s1",
4116 "parent_node",
4117 vec!["b1".into(), "b2".into()],
4118 vec!["child1".into(), "child2".into()],
4119 "Parent failed verification",
4120 );
4121 assert!(flush.flush_id.starts_with("flush_"));
4122 assert_eq!(flush.flushed_branch_ids.len(), 2);
4123 assert_eq!(flush.requeue_node_ids.len(), 2);
4124 assert!(flush.created_at > 0);
4125 }
4126
4127 #[test]
4128 fn test_blocked_dependency() {
4129 let dep = BlockedDependency::new("child_node", "parent_node", vec!["src/api.rs".into()]);
4130 assert_eq!(dep.child_node_id, "child_node");
4131 assert_eq!(dep.parent_node_id, "parent_node");
4132 assert_eq!(dep.required_seal_paths.len(), 1);
4133 assert!(dep.blocked_at > 0);
4134 }
4135
4136 #[test]
4137 fn test_srbn_node_phase6_fields() {
4138 let node = SRBNNode::new("n1".into(), "goal".into(), ModelTier::Actuator);
4139 assert!(node.provisional_branch_id.is_none());
4140 assert!(node.interface_seal_hash.is_none());
4141 }
4142
4143 #[test]
4148 fn test_plan_revision_initial() {
4149 let plan = TaskPlan {
4150 tasks: vec![PlannedTask::new("t1", "Do something")],
4151 };
4152 let rev = PlanRevision::initial("session_1", plan);
4153 assert_eq!(rev.sequence, 1);
4154 assert_eq!(rev.reason, "initial");
4155 assert!(rev.supersedes.is_none());
4156 assert!(rev.is_active());
4157 assert_eq!(rev.status, PlanRevisionStatus::Active);
4158 }
4159
4160 #[test]
4161 fn test_plan_revision_successor() {
4162 let plan1 = TaskPlan {
4163 tasks: vec![PlannedTask::new("t1", "First")],
4164 };
4165 let rev1 = PlanRevision::initial("s1", plan1);
4166
4167 let plan2 = TaskPlan {
4168 tasks: vec![PlannedTask::new("t2", "Second")],
4169 };
4170 let rev2 = PlanRevision::successor(&rev1, plan2, "verification_failure");
4171
4172 assert_eq!(rev2.sequence, 2);
4173 assert_eq!(rev2.reason, "verification_failure");
4174 assert_eq!(rev2.supersedes, Some(rev1.revision_id.clone()));
4175 assert!(rev2.is_active());
4176 }
4177
4178 #[test]
4179 fn test_plan_revision_status_display() {
4180 assert_eq!(PlanRevisionStatus::Active.to_string(), "active");
4181 assert_eq!(PlanRevisionStatus::Superseded.to_string(), "superseded");
4182 assert_eq!(PlanRevisionStatus::Cancelled.to_string(), "cancelled");
4183 }
4184
4185 #[test]
4186 fn test_planning_policy_defaults_and_queries() {
4187 let policy = PlanningPolicy::default();
4188 assert_eq!(policy, PlanningPolicy::FeatureIncrement);
4189 assert!(policy.needs_architect());
4190 assert!(!policy.needs_speculator());
4191
4192 assert!(!PlanningPolicy::LocalEdit.needs_architect());
4193 assert!(!PlanningPolicy::LocalEdit.needs_speculator());
4194
4195 assert!(PlanningPolicy::LargeFeature.needs_architect());
4196 assert!(PlanningPolicy::LargeFeature.needs_speculator());
4197
4198 assert!(PlanningPolicy::GreenfieldBuild.needs_architect());
4199 assert!(PlanningPolicy::GreenfieldBuild.needs_speculator());
4200
4201 assert!(PlanningPolicy::ArchitecturalRevision.needs_architect());
4202 assert!(PlanningPolicy::ArchitecturalRevision.needs_speculator());
4203 }
4204
4205 #[test]
4206 fn test_repair_footprint_creation() {
4207 let bundle = ArtifactBundle {
4208 artifacts: vec![ArtifactOperation::Write {
4209 path: "src/fix.rs".into(),
4210 content: "fixed".into(),
4211 }],
4212 commands: vec![],
4213 };
4214 let fp = RepairFootprint::new("s1", "node1", "rev1", 1, &bundle, "Syntax error");
4215 assert_eq!(fp.node_id, "node1");
4216 assert_eq!(fp.attempt, 1);
4217 assert_eq!(fp.affected_files, vec!["src/fix.rs"]);
4218 assert!(!fp.resolved);
4219
4220 let mut fp = fp;
4221 fp.mark_resolved();
4222 assert!(fp.resolved);
4223 }
4224
4225 #[test]
4226 fn test_budget_envelope_tracking() {
4227 let mut budget = BudgetEnvelope::new("s1");
4228 budget.max_steps = Some(3);
4229 budget.max_revisions = Some(2);
4230 budget.max_cost_usd = Some(1.0);
4231
4232 assert!(!budget.any_exhausted());
4233
4234 budget.record_step();
4235 budget.record_step();
4236 assert!(!budget.steps_exhausted());
4237 budget.record_step();
4238 assert!(budget.steps_exhausted());
4239 assert!(budget.any_exhausted());
4240 }
4241
4242 #[test]
4243 fn test_budget_envelope_cost_tracking() {
4244 let mut budget = BudgetEnvelope::new("s1");
4245 budget.max_cost_usd = Some(0.50);
4246 budget.record_cost(0.25);
4247 assert!(!budget.cost_exhausted());
4248 budget.record_cost(0.30);
4249 assert!(budget.cost_exhausted());
4250 }
4251
4252 #[test]
4253 fn test_artifact_operation_delete_and_move() {
4254 let del = ArtifactOperation::Delete {
4255 path: "src/old.rs".into(),
4256 };
4257 assert!(del.is_delete());
4258 assert!(!del.is_write());
4259 assert_eq!(del.path(), "src/old.rs");
4260
4261 let mv = ArtifactOperation::Move {
4262 from: "src/old.rs".into(),
4263 to: "src/new.rs".into(),
4264 };
4265 assert!(mv.is_move());
4266 assert!(!mv.is_write());
4267 assert_eq!(mv.path(), "src/old.rs");
4268 }
4269
4270 #[test]
4271 fn test_artifact_bundle_with_delete_and_move() {
4272 let bundle = ArtifactBundle {
4273 artifacts: vec![
4274 ArtifactOperation::Write {
4275 path: "src/new.rs".into(),
4276 content: "code".into(),
4277 },
4278 ArtifactOperation::Delete {
4279 path: "src/old.rs".into(),
4280 },
4281 ArtifactOperation::Move {
4282 from: "src/a.rs".into(),
4283 to: "src/b.rs".into(),
4284 },
4285 ],
4286 commands: vec![],
4287 };
4288 assert_eq!(bundle.writes_count(), 1);
4289 assert_eq!(bundle.deletes_count(), 1);
4290 assert_eq!(bundle.moves_count(), 1);
4291 assert!(bundle.validate().is_ok());
4292
4293 let paths = bundle.affected_paths();
4294 assert!(paths.contains(&"src/new.rs"));
4295 assert!(paths.contains(&"src/old.rs"));
4296 assert!(paths.contains(&"src/a.rs"));
4297 assert!(paths.contains(&"src/b.rs"));
4298 }
4299
4300 #[test]
4301 fn test_artifact_bundle_move_validation() {
4302 let bundle = ArtifactBundle {
4304 artifacts: vec![ArtifactOperation::Move {
4305 from: "src/a.rs".into(),
4306 to: "../outside.rs".into(),
4307 }],
4308 commands: vec![],
4309 };
4310 assert!(bundle.validate().is_err());
4311 }
4312
4313 #[test]
4314 fn test_dependency_expectation_default() {
4315 let de = DependencyExpectation::default();
4316 assert!(de.required_packages.is_empty());
4317 assert!(de.setup_commands.is_empty());
4318 assert!(de.min_toolchain_version.is_none());
4319 }
4320
4321 #[test]
4322 fn test_planned_task_has_dependency_expectations() {
4323 let task = PlannedTask::new("t1", "Build module");
4324 assert!(task.dependency_expectations.required_packages.is_empty());
4325 }
4326
4327 #[test]
4328 fn test_srbn_node_carries_dependency_expectations() {
4329 let mut task = PlannedTask::new("t1", "Build module");
4330 task.dependency_expectations = DependencyExpectation {
4331 required_packages: vec!["serde".to_string(), "tokio".to_string()],
4332 setup_commands: vec!["cargo fetch".to_string()],
4333 min_toolchain_version: Some("1.75".to_string()),
4334 };
4335 let node = task.to_srbn_node(ModelTier::Actuator);
4336 assert_eq!(node.dependency_expectations.required_packages.len(), 2);
4337 assert_eq!(node.dependency_expectations.required_packages[0], "serde");
4338 assert_eq!(node.dependency_expectations.setup_commands, ["cargo fetch"]);
4339 assert_eq!(
4340 node.dependency_expectations
4341 .min_toolchain_version
4342 .as_deref(),
4343 Some("1.75")
4344 );
4345 }
4346
4347 #[test]
4348 fn test_dependency_expectations_deserialized_from_json() {
4349 let json = r#"{
4350 "id": "t1",
4351 "goal": "Build module",
4352 "dependency_expectations": {
4353 "required_packages": ["requests", "pydantic"],
4354 "setup_commands": [],
4355 "min_toolchain_version": "3.11"
4356 }
4357 }"#;
4358 let task: PlannedTask = serde_json::from_str(json).unwrap();
4359 assert_eq!(task.dependency_expectations.required_packages.len(), 2);
4360 assert_eq!(
4361 task.dependency_expectations
4362 .min_toolchain_version
4363 .as_deref(),
4364 Some("3.11")
4365 );
4366 }
4367
4368 #[test]
4369 fn test_dependency_expectations_default_when_omitted() {
4370 let json = r#"{"id": "t1", "goal": "Build module"}"#;
4371 let task: PlannedTask = serde_json::from_str(json).unwrap();
4372 assert!(task.dependency_expectations.required_packages.is_empty());
4373 assert!(task.dependency_expectations.setup_commands.is_empty());
4374 assert!(task.dependency_expectations.min_toolchain_version.is_none());
4375 }
4376
4377 #[test]
4378 fn test_node_state_from_display_str_case_insensitive() {
4379 assert_eq!(
4380 NodeState::from_display_str("Completed"),
4381 NodeState::Completed
4382 );
4383 assert_eq!(
4384 NodeState::from_display_str("COMPLETED"),
4385 NodeState::Completed
4386 );
4387 assert_eq!(
4388 NodeState::from_display_str("completed"),
4389 NodeState::Completed
4390 );
4391 assert_eq!(
4392 NodeState::from_display_str("TaskQueued"),
4393 NodeState::TaskQueued
4394 );
4395 assert_eq!(
4396 NodeState::from_display_str("TASKQUEUED"),
4397 NodeState::TaskQueued
4398 );
4399 assert_eq!(NodeState::from_display_str("coding"), NodeState::Coding);
4400 assert_eq!(NodeState::from_display_str("STABLE"), NodeState::Completed);
4401 assert_eq!(NodeState::from_display_str("RUNNING"), NodeState::Coding);
4402 assert_eq!(
4404 NodeState::from_display_str("garbage"),
4405 NodeState::TaskQueued
4406 );
4407 }
4408
4409 #[test]
4410 fn test_node_state_display_roundtrip() {
4411 let states = [
4412 NodeState::TaskQueued,
4413 NodeState::Planning,
4414 NodeState::Coding,
4415 NodeState::Verifying,
4416 NodeState::Retry,
4417 NodeState::SheafCheck,
4418 NodeState::Committing,
4419 NodeState::Escalated,
4420 NodeState::Completed,
4421 NodeState::Failed,
4422 ];
4423 for state in &states {
4424 let display = state.to_string();
4425 let parsed = NodeState::from_display_str(&display);
4426 assert_eq!(parsed, *state, "Roundtrip failed for {:?}", state);
4427 }
4428 }
4429
4430 #[test]
4431 fn test_node_state_is_success() {
4432 assert!(NodeState::Completed.is_success());
4433 assert!(!NodeState::Escalated.is_success());
4434 assert!(!NodeState::Failed.is_success());
4435 assert!(!NodeState::Coding.is_success());
4436 }
4437
4438 #[test]
4439 fn test_node_state_is_active() {
4440 assert!(NodeState::Coding.is_active());
4441 assert!(NodeState::Verifying.is_active());
4442 assert!(NodeState::Planning.is_active());
4443 assert!(NodeState::Retry.is_active());
4444 assert!(NodeState::SheafCheck.is_active());
4445 assert!(NodeState::Committing.is_active());
4446 assert!(!NodeState::Completed.is_active());
4447 assert!(!NodeState::Escalated.is_active());
4448 assert!(!NodeState::TaskQueued.is_active());
4449 }
4450
4451 #[test]
4452 fn test_session_outcome_equality() {
4453 assert_eq!(SessionOutcome::Success, SessionOutcome::Success);
4454 assert_ne!(SessionOutcome::Success, SessionOutcome::PartialSuccess);
4455 assert_ne!(SessionOutcome::Success, SessionOutcome::Failed);
4456 assert_ne!(SessionOutcome::PartialSuccess, SessionOutcome::Failed);
4457 }
4458
4459 #[test]
4462 fn test_parse_result_state_is_ok() {
4463 assert!(ParseResultState::StrictJsonOk.is_ok());
4464 assert!(ParseResultState::TolerantRecoveryOk.is_ok());
4465 assert!(!ParseResultState::NoStructuredPayload.is_ok());
4466 assert!(!ParseResultState::SchemaInvalid.is_ok());
4467 assert!(!ParseResultState::SemanticallyRejected.is_ok());
4468 assert!(!ParseResultState::EmptyBundle.is_ok());
4469 }
4470
4471 #[test]
4472 fn test_parse_result_state_display() {
4473 assert_eq!(ParseResultState::StrictJsonOk.to_string(), "strict_json_ok");
4474 assert_eq!(
4475 ParseResultState::NoStructuredPayload.to_string(),
4476 "no_structured_payload"
4477 );
4478 assert_eq!(
4479 ParseResultState::SemanticallyRejected.to_string(),
4480 "semantically_rejected"
4481 );
4482 }
4483
4484 #[test]
4485 fn test_retry_classification_display() {
4486 assert_eq!(
4487 RetryClassification::MalformedRetry.to_string(),
4488 "malformed_retry"
4489 );
4490 assert_eq!(RetryClassification::Retarget.to_string(), "retarget");
4491 assert_eq!(RetryClassification::Replan.to_string(), "replan");
4492 assert_eq!(
4493 RetryClassification::BudgetExhausted.to_string(),
4494 "budget_exhausted"
4495 );
4496 }
4497
4498 #[test]
4499 fn test_prompt_intent_serde_roundtrip() {
4500 let intents = [
4501 PromptIntent::ArchitectExisting,
4502 PromptIntent::ActuatorMultiOutput,
4503 PromptIntent::CorrectionRetry,
4504 PromptIntent::SoloGenerate,
4505 ];
4506 for intent in &intents {
4507 let json = serde_json::to_string(intent).unwrap();
4508 let back: PromptIntent = serde_json::from_str(&json).unwrap();
4509 assert_eq!(*intent, back);
4510 }
4511 }
4512
4513 #[test]
4514 fn test_task_plan_cycle_detection() {
4515 let mut a = PlannedTask::new("a", "goal a");
4516 a.dependencies = vec!["b".to_string()];
4517 let mut b = PlannedTask::new("b", "goal b");
4518 b.dependencies = vec!["c".to_string()];
4519 let mut c = PlannedTask::new("c", "goal c");
4520 c.dependencies = vec!["a".to_string()];
4521 let plan = TaskPlan {
4522 tasks: vec![a, b, c],
4523 };
4524 let err = plan.validate().unwrap_err();
4525 assert!(err.contains("cycle"), "Expected cycle error, got: {err}");
4526 }
4527
4528 #[test]
4529 fn test_task_plan_implicit_dependency_enforcement() {
4530 let mut a = PlannedTask::new("a", "use lib");
4532 a.context_files = vec!["src/lib.rs".to_string()];
4533 a.output_files = vec!["src/main.rs".to_string()];
4534 let mut b = PlannedTask::new("b", "create lib");
4535 b.output_files = vec!["src/lib.rs".to_string()];
4536
4537 let mut plan = TaskPlan { tasks: vec![a, b] };
4538 let err = plan.validate().unwrap_err();
4539 assert!(
4540 err.contains("does not declare it as a dependency"),
4541 "Expected implicit dep error, got: {err}"
4542 );
4543 plan.tasks[0].dependencies.push("b".to_string());
4545 assert!(plan.validate().is_ok());
4546 }
4547
4548 #[test]
4549 fn test_task_plan_valid_acyclic() {
4550 let a = PlannedTask::new("a", "goal a");
4551 let mut b = PlannedTask::new("b", "goal b");
4552 b.dependencies = vec!["a".to_string()];
4553 let mut c = PlannedTask::new("c", "goal c");
4554 c.dependencies = vec!["a".to_string(), "b".to_string()];
4555 let plan = TaskPlan {
4556 tasks: vec![a, b, c],
4557 };
4558 assert!(plan.validate().is_ok());
4559 }
4560
4561 #[test]
4562 fn test_task_plan_test_file_dependency_inference() {
4563 let mut src = PlannedTask::new("src", "implement lib");
4566 src.output_files = vec!["src/lib.rs".to_string()];
4567 let mut tst = PlannedTask::new("tst", "test lib");
4568 tst.output_files = vec!["tests/lib_test.rs".to_string()];
4569
4570 let plan = TaskPlan {
4571 tasks: vec![src, tst],
4572 };
4573 let err = plan.validate().unwrap_err();
4574 assert!(
4575 err.contains("Test task 'tst'") && err.contains("does not depend on source task 'src'"),
4576 "Expected test-dep inference error, got: {err}"
4577 );
4578 }
4579
4580 #[test]
4581 fn test_task_plan_test_file_dependency_satisfied() {
4582 let mut src = PlannedTask::new("src", "implement lib");
4583 src.output_files = vec!["src/lib.rs".to_string()];
4584 let mut tst = PlannedTask::new("tst", "test lib");
4585 tst.output_files = vec!["tests/lib_test.rs".to_string()];
4586 tst.dependencies = vec!["src".to_string()];
4587
4588 let plan = TaskPlan {
4589 tasks: vec![src, tst],
4590 };
4591 assert!(plan.validate().is_ok());
4592 }
4593
4594 #[test]
4595 fn test_glob_matches_simple() {
4596 assert!(super::glob_matches_simple("tests/*.rs", "tests/foo.rs"));
4597 assert!(!super::glob_matches_simple("tests/*.rs", "src/foo.rs"));
4598 assert!(super::glob_matches_simple(
4599 "**/*.test.js",
4600 "src/utils.test.js"
4601 ));
4602 assert!(super::glob_matches_simple("test_*.py", "test_auth.py"));
4603 assert!(!super::glob_matches_simple("test_*.py", "auth.py"));
4604 assert!(super::glob_matches_simple(
4605 "tests/**/*.rs",
4606 "tests/unit/foo.rs"
4607 ));
4608 }
4609}