1use super::*;
4
5impl SRBNOrchestrator {
6 pub(super) async fn step_verify(&mut self, idx: NodeIndex) -> Result<EnergyComponents> {
10 log::info!("Step 4: Verification - Computing stability energy");
11
12 self.last_verification_result = None;
15 self.context.last_test_output = None;
18
19 self.graph[idx].state = NodeState::Verifying;
20 self.emit_event(perspt_core::AgentEvent::TaskStatusChanged {
21 node_id: self.graph[idx].node_id.clone(),
22 status: perspt_core::NodeStatus::Verifying,
23 });
24
25 let mut energy = EnergyComponents::default();
27
28 if let Some(ref err) = self.last_tool_failure {
30 energy.v_syn = 10.0; log::warn!("Tool failure detected, V_syn set to 10.0: {}", err);
32 self.emit_log(format!("β οΈ Tool failure prevents verification: {}", err));
33 self.context.last_diagnostics = vec![lsp_types::Diagnostic {
37 range: lsp_types::Range::default(),
38 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
39 code: None,
40 code_description: None,
41 source: Some("tool".to_string()),
42 message: format!("Failed to apply changes: {}", err),
43 related_information: None,
44 tags: None,
45 data: None,
46 }];
47 }
48
49 if let Some(ref path) = self.last_written_file {
51 let node_plugin = self.graph[idx].owner_plugin.clone();
53 let lsp_key = if node_plugin.is_empty() || node_plugin == "unknown" {
54 "python".to_string() } else {
56 node_plugin
57 };
58
59 if let Some(client) = self.lsp_clients.get(&lsp_key) {
60 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
62
63 let path_str = path.to_string_lossy().to_string();
64 let diagnostics = client.get_diagnostics(&path_str).await;
65
66 if !diagnostics.is_empty() {
67 energy.v_syn = LspClient::calculate_syntactic_energy(&diagnostics);
68 log::info!(
69 "LSP found {} diagnostics, V_syn={:.2}",
70 diagnostics.len(),
71 energy.v_syn
72 );
73 self.emit_log(format!("π LSP found {} diagnostics:", diagnostics.len()));
74 for d in &diagnostics {
75 self.emit_log(format!(
76 " - [{}] {}",
77 severity_to_str(d.severity),
78 d.message
79 ));
80 }
81
82 self.context.last_diagnostics = diagnostics;
84 } else {
85 log::info!("LSP reports no errors (diagnostics vector is empty)");
86 }
87 } else {
88 log::debug!("No LSP client available for plugin '{}'", lsp_key);
89 }
90
91 let node = &self.graph[idx];
93 if !node.contract.forbidden_patterns.is_empty() {
94 if let Ok(content) = std::fs::read_to_string(path) {
95 for pattern in &node.contract.forbidden_patterns {
96 if content.contains(pattern) {
97 energy.v_str += 0.5;
98 log::warn!("Forbidden pattern found: '{}'", pattern);
99 self.emit_log(format!("β οΈ Forbidden pattern: '{}'", pattern));
100 }
101 }
102 }
103 }
104
105 let node = &self.graph[idx];
108 if self.context.defer_tests {
109 self.emit_log("βοΈ Tests deferred (--defer-tests enabled)".to_string());
110 } else {
111 let plugin_name = node.owner_plugin.clone();
112 let verify_dir = self.effective_working_dir(idx);
113 let stages = verification_stages_for_node(node);
114
115 if !stages.is_empty() && !plugin_name.is_empty() && plugin_name != "unknown" {
116 let dep_exp = node.dependency_expectations.clone();
121 if !dep_exp.required_packages.is_empty() {
122 self.emit_log(format!(
123 "π¦ Pre-installing declared dependencies: {}",
124 dep_exp.required_packages.join(", ")
125 ));
126 let installed = if plugin_name == "python" {
127 Self::auto_install_python_deps(&dep_exp.required_packages, &verify_dir)
128 .await
129 } else {
130 Self::auto_install_crate_deps(&dep_exp.required_packages, &verify_dir)
131 .await
132 };
133 if installed > 0 {
134 self.emit_log(format!(
135 "π¦ Pre-installed {} declared package(s)",
136 installed
137 ));
138 }
139 }
140
141 self.emit_log(format!(
142 "π¬ Running verification ({} stages) for {} node '{}'...",
143 stages.len(),
144 node.node_class,
145 node.node_id
146 ));
147
148 let mut vr = self
149 .run_plugin_verification(&plugin_name, &stages, verify_dir.clone())
150 .await;
151
152 if !vr.syntax_ok || !vr.build_ok {
155 if let Some(ref raw) = vr.raw_output {
156 let missing = Self::extract_missing_crates(raw);
157 if !missing.is_empty() {
158 self.emit_log(format!(
159 "π¦ Auto-installing missing dependencies: {}",
160 missing.join(", ")
161 ));
162 let dep_ok =
163 Self::auto_install_crate_deps(&missing, &verify_dir).await;
164 if dep_ok > 0 {
165 self.emit_log(format!(
166 "π¦ Installed {} crate(s), re-running verification...",
167 dep_ok
168 ));
169 vr = self
171 .run_plugin_verification(
172 &plugin_name,
173 &stages,
174 verify_dir.clone(),
175 )
176 .await;
177 }
178 }
179 }
180 }
181
182 if plugin_name == "python" && (!vr.syntax_ok || !vr.tests_ok) {
186 let all_output: String = vr
190 .stage_outcomes
191 .iter()
192 .filter_map(|so| so.output.as_deref())
193 .collect::<Vec<_>>()
194 .join("\n");
195 let combined = match vr.raw_output.as_deref() {
196 Some(raw) => format!("{}\n{}", raw, all_output),
197 None => all_output,
198 };
199
200 let missing = Self::extract_missing_python_modules(&combined);
201 if !missing.is_empty() {
202 self.emit_log(format!(
203 "π Auto-installing missing Python packages: {}",
204 missing.join(", ")
205 ));
206 let dep_ok =
207 Self::auto_install_python_deps(&missing, &verify_dir).await;
208 if dep_ok > 0 {
209 self.emit_log(format!(
210 "π Installed {} package(s), re-running verification...",
211 dep_ok
212 ));
213 vr = self
214 .run_plugin_verification(
215 &plugin_name,
216 &stages,
217 verify_dir.clone(),
218 )
219 .await;
220 }
221 }
222 }
223
224 if !vr.syntax_ok && energy.v_syn < 5.0 {
227 energy.v_syn = 5.0;
228 }
229 if !vr.build_ok && energy.v_syn < 8.0 {
231 energy.v_syn = 8.0;
232 }
233 if !vr.tests_ok && vr.tests_failed > 0 {
235 let node = &self.graph[idx];
236 if !node.contract.weighted_tests.is_empty() {
237 let py_runner = PythonTestRunner::new(verify_dir);
239 let test_results = TestResults {
240 passed: vr.tests_passed,
241 failed: vr.tests_failed,
242 total: vr.tests_passed + vr.tests_failed,
243 output: vr.raw_output.clone().unwrap_or_default(),
244 failures: Vec::new(),
245 run_succeeded: true,
246 skipped: 0,
247 duration_ms: 0,
248 };
249 energy.v_log = py_runner.calculate_v_log(&test_results, &node.contract);
250 } else {
251 let total = (vr.tests_passed + vr.tests_failed) as f32;
253 if total > 0.0 {
254 energy.v_log = (vr.tests_failed as f32 / total) * 10.0;
255 }
256 }
257 }
258 if !vr.tests_ok
263 && vr.tests_failed == 0
264 && vr.tests_passed == 0
265 && stages.contains(&perspt_core::plugin::VerifierStage::Test)
266 {
267 let test_compile_failed = vr
269 .raw_output
270 .as_deref()
271 .or_else(|| {
272 vr.stage_outcomes
273 .iter()
274 .find(|so| so.stage == "test")
275 .and_then(|so| so.output.as_deref())
276 })
277 .is_some_and(|o| {
278 o.contains("error[E")
279 || o.contains("could not compile")
280 || o.contains("FAILED")
281 || o.contains("ModuleNotFoundError")
282 || o.contains("ImportError")
283 });
284 if test_compile_failed {
285 log::warn!(
286 "Test compilation failed for node '{}' β treating as build failure",
287 self.graph[idx].node_id
288 );
289 if energy.v_syn < 8.0 {
290 energy.v_syn = 8.0;
291 }
292 } else {
293 energy.v_log = 5.0;
295 log::warn!(
296 "Tests expected but did not produce results for node '{}'",
297 self.graph[idx].node_id
298 );
299 }
300 }
301 if !vr.lint_ok
303 && self.context.verifier_strictness
304 == perspt_core::types::VerifierStrictness::Strict
305 {
306 energy.v_str += 0.3;
307 }
308
309 if vr.degraded && vr.stage_outcomes.is_empty() {
315 energy.v_boot = 10.0;
317 log::warn!(
318 "V_boot = 10.0: toolchain fully degraded ({})",
319 vr.degraded_reason.as_deref().unwrap_or("unknown")
320 );
321 }
322 for so in &vr.stage_outcomes {
323 match &so.sensor_status {
324 perspt_core::types::SensorStatus::Unavailable { reason } => {
325 energy.v_boot += 3.0;
326 log::warn!(
327 "V_boot +3.0: sensor unavailable for stage '{}': {}",
328 so.stage,
329 reason
330 );
331 }
332 perspt_core::types::SensorStatus::Fallback { reason, .. } => {
333 energy.v_boot += 1.0;
334 log::info!(
335 "V_boot +1.0: fallback sensor for stage '{}': {}",
336 so.stage,
337 reason
338 );
339 }
340 perspt_core::types::SensorStatus::Available => {}
341 }
342 }
343
344 if let Some(ref raw) = vr.raw_output {
346 self.context.last_test_output = Some(raw.clone());
347 }
348
349 self.emit_log(format!("π Verification: {}", vr.summary));
350 }
351 }
352 }
353
354 let node = &self.graph[idx];
355 if let Err(e) =
357 self.ledger
358 .record_energy(&node.node_id, &energy, energy.total(&node.contract))
359 {
360 log::error!("Failed to record energy: {}", e);
361 }
362
363 log::info!(
364 "Energy for {}: V_syn={:.2}, V_str={:.2}, V_log={:.2}, V_boot={:.2}, V_sheaf={:.2}, Total={:.2}",
365 node.node_id,
366 energy.v_syn,
367 energy.v_str,
368 energy.v_log,
369 energy.v_boot,
370 energy.v_sheaf,
371 energy.total(&node.contract)
372 );
373
374 {
376 let node = &self.graph[idx];
377 let total = energy.total(&node.contract);
378 let (
379 stage_outcomes,
380 degraded,
381 degraded_reasons,
382 summary,
383 lint_ok,
384 tests_passed,
385 tests_failed,
386 ) = if let Some(ref vr) = self.last_verification_result {
387 (
388 vr.stage_outcomes.clone(),
389 vr.degraded,
390 vr.degraded_stage_reasons(),
391 vr.summary.clone(),
392 vr.lint_ok,
393 vr.tests_passed,
394 vr.tests_failed,
395 )
396 } else {
397 let diag_count = self.context.last_diagnostics.len();
398 (
399 Vec::new(),
400 false,
401 Vec::new(),
402 format!("V(x)={:.2} | {} diagnostics", total, diag_count),
403 true,
404 0,
405 0,
406 )
407 };
408
409 self.emit_event(perspt_core::AgentEvent::VerificationComplete {
410 node_id: node.node_id.clone(),
411 syntax_ok: energy.v_syn == 0.0,
412 build_ok: energy.v_syn < 5.0,
413 tests_ok: energy.v_log == 0.0,
414 lint_ok,
415 diagnostics_count: self.context.last_diagnostics.len(),
416 tests_passed,
417 tests_failed,
418 energy: total,
419 energy_components: energy.clone(),
420 stage_outcomes,
421 degraded,
422 degraded_reasons,
423 summary,
424 node_class: node.node_class.to_string(),
425 });
426 }
427
428 Ok(energy)
429 }
430
431 pub async fn run_plugin_verification(
441 &mut self,
442 plugin_name: &str,
443 allowed_stages: &[perspt_core::plugin::VerifierStage],
444 working_dir: std::path::PathBuf,
445 ) -> perspt_core::types::VerificationResult {
446 use perspt_core::plugin::VerifierStage;
447 use perspt_core::types::{SensorStatus, StageOutcome};
448
449 let registry = perspt_core::plugin::PluginRegistry::new();
450 let plugin = match registry.get(plugin_name) {
451 Some(p) => p,
452 None => {
453 return perspt_core::types::VerificationResult::degraded(format!(
454 "Plugin '{}' not found",
455 plugin_name
456 ));
457 }
458 };
459
460 let profile = plugin.verifier_profile();
461
462 if profile.fully_degraded() {
464 return perspt_core::types::VerificationResult::degraded(format!(
465 "{} toolchain not available on host (all stages degraded)",
466 plugin.name()
467 ));
468 }
469
470 let sensor_status_for = |stage: VerifierStage,
472 profile: &perspt_core::plugin::VerifierProfile|
473 -> SensorStatus {
474 match profile.get(stage) {
475 Some(cap) if cap.available => SensorStatus::Available,
476 Some(cap) if cap.fallback_available => SensorStatus::Fallback {
477 actual: cap
478 .fallback_command
479 .clone()
480 .unwrap_or_else(|| "fallback".into()),
481 reason: format!(
482 "primary '{}' not found",
483 cap.command.as_deref().unwrap_or("?")
484 ),
485 },
486 Some(cap) => SensorStatus::Unavailable {
487 reason: format!(
488 "no tool for {} (tried '{}')",
489 stage,
490 cap.command.as_deref().unwrap_or("?")
491 ),
492 },
493 None => SensorStatus::Unavailable {
494 reason: format!("{} stage not declared by plugin", stage),
495 },
496 }
497 };
498
499 let syn_sensor = sensor_status_for(VerifierStage::SyntaxCheck, &profile);
500 let build_sensor = sensor_status_for(VerifierStage::Build, &profile);
501 let test_sensor = sensor_status_for(VerifierStage::Test, &profile);
502 let lint_sensor = sensor_status_for(VerifierStage::Lint, &profile);
503
504 let runner = test_runner::test_runner_for_profile(profile, working_dir);
505
506 let mut result = perspt_core::types::VerificationResult::default();
507
508 if allowed_stages.contains(&VerifierStage::SyntaxCheck) {
514 match runner.run_syntax_check().await {
515 Ok(r) => {
516 result.syntax_ok = r.passed > 0 && r.failed == 0;
517 if !result.syntax_ok && r.run_succeeded {
518 result.diagnostics_count = r.output.lines().count();
519 result.raw_output = Some(r.output.clone());
520 self.emit_log(format!(
521 "β οΈ Syntax check failed ({} diagnostics)",
522 result.diagnostics_count
523 ));
524 } else if result.syntax_ok {
525 self.emit_log("β
Syntax check passed".to_string());
526 }
527 result.stage_outcomes.push(StageOutcome {
528 stage: VerifierStage::SyntaxCheck.to_string(),
529 passed: result.syntax_ok,
530 sensor_status: syn_sensor,
531 output: Some(r.output),
532 });
533 }
534 Err(e) => {
535 log::warn!("Syntax check failed to run: {}", e);
536 result.syntax_ok = false;
537 result.stage_outcomes.push(StageOutcome {
538 stage: VerifierStage::SyntaxCheck.to_string(),
539 passed: false,
540 sensor_status: SensorStatus::Unavailable {
541 reason: format!("execution error: {}", e),
542 },
543 output: None,
544 });
545 }
546 }
547
548 if !result.syntax_ok {
550 self.emit_log("βοΈ Skipping build/test/lint β syntax check failed".to_string());
551 result.build_ok = false;
552 result.tests_ok = false;
553 self.finalize_verification_result(&mut result, plugin_name);
554 return result;
555 }
556 }
557
558 if allowed_stages.contains(&VerifierStage::Build) {
560 match runner.run_build_check().await {
561 Ok(r) => {
562 result.build_ok = r.passed > 0 && r.failed == 0;
563 if result.build_ok {
564 self.emit_log("β
Build passed".to_string());
565 } else if r.run_succeeded {
566 self.emit_log("β οΈ Build failed".to_string());
567 result.raw_output = Some(r.output.clone());
568 }
569 result.stage_outcomes.push(StageOutcome {
570 stage: VerifierStage::Build.to_string(),
571 passed: result.build_ok,
572 sensor_status: build_sensor,
573 output: Some(r.output),
574 });
575 }
576 Err(e) => {
577 log::warn!("Build check failed to run: {}", e);
578 result.build_ok = false;
579 result.stage_outcomes.push(StageOutcome {
580 stage: VerifierStage::Build.to_string(),
581 passed: false,
582 sensor_status: SensorStatus::Unavailable {
583 reason: format!("execution error: {}", e),
584 },
585 output: None,
586 });
587 }
588 }
589
590 if !result.build_ok {
592 self.emit_log("βοΈ Skipping test/lint β build failed".to_string());
593 result.tests_ok = false;
594 self.finalize_verification_result(&mut result, plugin_name);
595 return result;
596 }
597 }
598
599 if allowed_stages.contains(&VerifierStage::Test) {
601 match runner.run_tests().await {
602 Ok(r) => {
603 result.tests_ok = r.all_passed();
604 result.tests_passed = r.passed;
605 result.tests_failed = r.failed;
606
607 if result.tests_ok {
608 self.emit_log(format!("β
Tests passed ({})", plugin_name));
609 } else {
610 self.emit_log(format!("β Tests failed ({})", plugin_name));
611 result.raw_output = Some(r.output.clone());
612 }
613 result.stage_outcomes.push(StageOutcome {
614 stage: VerifierStage::Test.to_string(),
615 passed: result.tests_ok,
616 sensor_status: test_sensor,
617 output: Some(r.output),
618 });
619 }
620 Err(e) => {
621 log::warn!("Test command failed to run: {}", e);
622 result.tests_ok = false;
623 result.stage_outcomes.push(StageOutcome {
624 stage: VerifierStage::Test.to_string(),
625 passed: false,
626 sensor_status: SensorStatus::Unavailable {
627 reason: format!("execution error: {}", e),
628 },
629 output: None,
630 });
631 }
632 }
633 } else {
634 result.tests_ok = true; }
636
637 if allowed_stages.contains(&VerifierStage::Lint)
639 && self.context.verifier_strictness == perspt_core::types::VerifierStrictness::Strict
640 {
641 match runner.run_lint().await {
642 Ok(r) => {
643 result.lint_ok = r.passed > 0 && r.failed == 0;
644 if result.lint_ok {
645 self.emit_log("β
Lint passed".to_string());
646 } else if r.run_succeeded {
647 self.emit_log("β οΈ Lint issues found".to_string());
648 }
649 result.stage_outcomes.push(StageOutcome {
650 stage: VerifierStage::Lint.to_string(),
651 passed: result.lint_ok,
652 sensor_status: lint_sensor,
653 output: Some(r.output),
654 });
655 }
656 Err(e) => {
657 log::warn!("Lint command failed to run: {}", e);
658 result.lint_ok = false;
659 result.stage_outcomes.push(StageOutcome {
660 stage: VerifierStage::Lint.to_string(),
661 passed: false,
662 sensor_status: SensorStatus::Unavailable {
663 reason: format!("execution error: {}", e),
664 },
665 output: None,
666 });
667 }
668 }
669 } else if !allowed_stages.contains(&VerifierStage::Lint) {
670 result.lint_ok = true; } else {
672 result.lint_ok = true; }
674
675 self.finalize_verification_result(&mut result, plugin_name);
676 result
677 }
678
679 fn extract_missing_crates(output: &str) -> Vec<String> {
689 use std::collections::HashSet;
690
691 let mut crates: HashSet<String> = HashSet::new();
692
693 for line in output.lines() {
694 let lower = line.to_lowercase();
695
696 if lower.contains("undeclared crate or module") {
698 if let Some(name) = Self::extract_backtick_ident(line) {
699 if !name.contains("::") {
700 crates.insert(name);
701 }
702 }
703 }
704 else if lower.contains("can't find crate for")
706 || lower.contains("cant find crate for")
707 {
708 if let Some(name) = Self::extract_backtick_ident(line) {
709 crates.insert(name);
710 }
711 }
712 else if lower.contains("unresolved import") {
714 if let Some(name) = Self::extract_backtick_ident(line) {
715 let root = name.split("::").next().unwrap_or(&name).to_string();
716 if root != "crate" && root != "self" && root != "super" {
717 crates.insert(root);
718 }
719 }
720 }
721 }
722
723 let builtins: HashSet<&str> = ["std", "core", "alloc", "proc_macro", "test"]
724 .iter()
725 .copied()
726 .collect();
727
728 crates
729 .into_iter()
730 .filter(|c| !builtins.contains(c.as_str()))
731 .collect()
732 }
733
734 fn extract_backtick_ident(line: &str) -> Option<String> {
736 let start = line.find('`')? + 1;
737 let rest = &line[start..];
738 let end = rest.find('`')?;
739 let ident = &rest[..end];
740 if ident.is_empty() {
741 None
742 } else {
743 Some(ident.to_string())
744 }
745 }
746
747 pub(super) fn extract_commands_from_correction(
752 response: &str,
753 owner_plugin: &str,
754 ) -> Vec<String> {
755 let registry = perspt_core::plugin::PluginRegistry::new();
756 let plugin = registry.get(owner_plugin);
757
758 let mut commands = Vec::new();
759 let mut in_commands_section = false;
760 let mut in_code_block = false;
761
762 for line in response.lines() {
763 let trimmed = line.trim();
764
765 if trimmed.starts_with("Commands:")
766 || trimmed.starts_with("**Commands:")
767 || trimmed.starts_with("### Commands")
768 {
769 in_commands_section = true;
770 continue;
771 }
772
773 if in_commands_section {
774 if trimmed.starts_with("```") {
775 in_code_block = !in_code_block;
776 continue;
777 }
778
779 if !in_code_block
780 && (trimmed.is_empty()
781 || trimmed.starts_with('#')
782 || trimmed.starts_with("File:")
783 || trimmed.starts_with("Diff:"))
784 {
785 in_commands_section = false;
786 continue;
787 }
788
789 let cmd = trimmed
790 .trim_start_matches("- ")
791 .trim_start_matches("$ ")
792 .trim();
793
794 if !cmd.is_empty() {
795 let decision = plugin
796 .map(|p| p.dependency_command_policy(cmd))
797 .unwrap_or(perspt_core::types::CommandPolicyDecision::Allow);
798
799 match decision {
800 perspt_core::types::CommandPolicyDecision::Allow
801 | perspt_core::types::CommandPolicyDecision::RequireApproval => {
802 commands.push(cmd.to_string());
803 }
804 perspt_core::types::CommandPolicyDecision::Deny => {
805 log::warn!(
806 "Command '{}' denied by plugin policy for '{}'",
807 cmd,
808 owner_plugin
809 );
810 }
811 }
812 }
813 }
814 }
815
816 commands
817 }
818
819 async fn auto_install_crate_deps(crates: &[String], working_dir: &std::path::Path) -> usize {
821 let mut installed = 0usize;
822 for krate in crates {
823 log::info!("Auto-installing crate: cargo add {}", krate);
824 let result = tokio::process::Command::new("cargo")
825 .args(["add", krate])
826 .current_dir(working_dir)
827 .stdout(std::process::Stdio::piped())
828 .stderr(std::process::Stdio::piped())
829 .output()
830 .await;
831
832 match result {
833 Ok(output) if output.status.success() => {
834 log::info!("Successfully installed crate: {}", krate);
835 installed += 1;
836 }
837 Ok(output) => {
838 let stderr = String::from_utf8_lossy(&output.stderr);
839 log::warn!("Failed to install crate {}: {}", krate, stderr);
840 }
841 Err(e) => {
842 log::warn!("Failed to run cargo add {}: {}", krate, e);
843 }
844 }
845 }
846 installed
847 }
848
849 pub(super) fn extract_missing_python_modules(output: &str) -> Vec<String> {
860 use std::collections::HashSet;
861
862 let mut modules: HashSet<String> = HashSet::new();
863
864 for line in output.lines() {
865 let trimmed = line.trim().trim_start_matches("E").trim();
866
867 if trimmed.contains("ModuleNotFoundError: No module named ") {
871 if let Some(pos) = trimmed.find("No module named ") {
873 let after = &trimmed[pos + "No module named ".len()..];
874 let name = after.trim().trim_matches('\'').trim_matches('"');
875 let root = name.split('.').next().unwrap_or(name);
876 if !root.is_empty() {
877 modules.insert(root.to_string());
878 }
879 }
880 }
881 else if trimmed.contains("ImportError") && trimmed.contains("No module named") {
884 if let Some(start) = trimmed.find('\'') {
885 let rest = &trimmed[start + 1..];
886 if let Some(end) = rest.find('\'') {
887 let name = &rest[..end];
888 let root = name.split('.').next().unwrap_or(name);
889 if !root.is_empty() {
890 modules.insert(root.to_string());
891 }
892 }
893 }
894 }
895 }
896
897 let stdlib: HashSet<&str> = [
899 "os",
900 "sys",
901 "json",
902 "re",
903 "math",
904 "datetime",
905 "collections",
906 "itertools",
907 "functools",
908 "pathlib",
909 "typing",
910 "abc",
911 "io",
912 "unittest",
913 "logging",
914 "argparse",
915 "sqlite3",
916 "csv",
917 "hashlib",
918 "tempfile",
919 "shutil",
920 "copy",
921 "contextlib",
922 "dataclasses",
923 "enum",
924 "textwrap",
925 "importlib",
926 "inspect",
927 "traceback",
928 "subprocess",
929 "threading",
930 "multiprocessing",
931 "asyncio",
932 "socket",
933 "http",
934 "urllib",
935 "xml",
936 "html",
937 "email",
938 "string",
939 "struct",
940 "array",
941 "queue",
942 "heapq",
943 "bisect",
944 "pprint",
945 "decimal",
946 "fractions",
947 "random",
948 "secrets",
949 "time",
950 "calendar",
951 "zlib",
952 "gzip",
953 "zipfile",
954 "tarfile",
955 "glob",
956 "fnmatch",
957 "stat",
958 "fileinput",
959 "codecs",
960 "uuid",
961 "base64",
962 "binascii",
963 "pickle",
964 "shelve",
965 "dbm",
966 "platform",
967 "signal",
968 "mmap",
969 "ctypes",
970 "configparser",
971 "tomllib",
972 "warnings",
973 "weakref",
974 "types",
975 "operator",
976 "numbers",
977 "__future__",
978 ]
979 .iter()
980 .copied()
981 .collect();
982
983 modules
984 .into_iter()
985 .filter(|m| !stdlib.contains(m.as_str()))
986 .collect()
987 }
988
989 pub(super) fn python_import_to_package(import_name: &str) -> &str {
994 match import_name {
995 "PIL" | "pil" => "pillow",
996 "cv2" => "opencv-python",
997 "yaml" => "pyyaml",
998 "bs4" => "beautifulsoup4",
999 "sklearn" => "scikit-learn",
1000 "attr" | "attrs" => "attrs",
1001 "dateutil" => "python-dateutil",
1002 "dotenv" => "python-dotenv",
1003 "gi" => "PyGObject",
1004 "serial" => "pyserial",
1005 "usb" => "pyusb",
1006 "wx" => "wxPython",
1007 "lxml" => "lxml",
1008 "Crypto" => "pycryptodome",
1009 "jose" => "python-jose",
1010 "jwt" => "PyJWT",
1011 "magic" => "python-magic",
1012 "docx" => "python-docx",
1013 "pptx" => "python-pptx",
1014 "git" => "gitpython",
1015 "psycopg2" => "psycopg2-binary",
1016 other => other,
1017 }
1018 }
1019
1020 async fn auto_install_python_deps(modules: &[String], working_dir: &std::path::Path) -> usize {
1022 let mut installed = 0usize;
1023 for module in modules {
1024 let package = Self::python_import_to_package(module);
1025 log::info!("Auto-installing Python package: uv add {}", package);
1026 let result = tokio::process::Command::new("uv")
1027 .args(["add", package])
1028 .current_dir(working_dir)
1029 .env_remove("VIRTUAL_ENV")
1030 .stdout(std::process::Stdio::piped())
1031 .stderr(std::process::Stdio::piped())
1032 .output()
1033 .await;
1034
1035 match result {
1036 Ok(output) if output.status.success() => {
1037 log::info!("Successfully installed Python package: {}", package);
1038 installed += 1;
1039 }
1040 Ok(output) => {
1041 let stderr = String::from_utf8_lossy(&output.stderr);
1042 log::warn!("Failed to install Python package {}: {}", package, stderr);
1043 }
1044 Err(e) => {
1045 log::warn!("Failed to run uv add {}: {}", package, e);
1046 }
1047 }
1048 }
1049
1050 if installed > 0 {
1052 log::info!("Running uv sync --dev after dependency install...");
1053 let _ = tokio::process::Command::new("uv")
1054 .args(["sync", "--dev"])
1055 .current_dir(working_dir)
1056 .env_remove("VIRTUAL_ENV")
1057 .stdout(std::process::Stdio::piped())
1058 .stderr(std::process::Stdio::piped())
1059 .output()
1060 .await;
1061 }
1062
1063 installed
1064 }
1065
1066 pub(super) fn normalize_command_to_uv(command: &str) -> String {
1071 let trimmed = command.trim();
1072
1073 let pip_install_prefixes = [
1078 "pip install ",
1079 "pip3 install ",
1080 "python -m pip install ",
1081 "python3 -m pip install ",
1082 ];
1083 for prefix in &pip_install_prefixes {
1084 if let Some(rest) = trimmed.strip_prefix(prefix) {
1085 let packages = rest.trim();
1086 if packages.is_empty() {
1087 return command.to_string();
1088 }
1089 if packages.starts_with("-r ") || packages.starts_with("--requirement ") {
1091 return format!("uv pip install {}", packages);
1092 }
1093 return format!("uv add {}", packages);
1094 }
1095 }
1096
1097 if trimmed.starts_with("pip install -") || trimmed.starts_with("pip3 install -") {
1099 return format!("uv {}", trimmed);
1100 }
1101
1102 command.to_string()
1103 }
1104
1105 fn finalize_verification_result(
1107 &mut self,
1108 result: &mut perspt_core::types::VerificationResult,
1109 plugin_name: &str,
1110 ) {
1111 if result.has_degraded_stages() {
1112 result.degraded = true;
1113 let reasons = result.degraded_stage_reasons();
1114 result.degraded_reason = Some(reasons.join("; "));
1115
1116 for outcome in &result.stage_outcomes {
1118 if let perspt_core::types::SensorStatus::Fallback { actual, reason } =
1119 &outcome.sensor_status
1120 {
1121 self.emit_event(perspt_core::AgentEvent::SensorFallback {
1122 node_id: plugin_name.to_string(),
1123 stage: outcome.stage.clone(),
1124 primary: reason.clone(),
1125 actual: actual.clone(),
1126 reason: reason.clone(),
1127 });
1128 }
1129 }
1130 }
1131
1132 self.last_verification_result = Some(result.clone());
1134
1135 result.summary = format!(
1137 "{}: syntax={}, build={}, tests={}, lint={}{}",
1138 plugin_name,
1139 if result.syntax_ok { "β
" } else { "β" },
1140 if result.build_ok { "β
" } else { "β" },
1141 if result.tests_ok { "β
" } else { "β" },
1142 if result.lint_ok { "β
" } else { "βοΈ" },
1143 if result.degraded { " (degraded)" } else { "" },
1144 );
1145 }
1146}
1147
1148pub(super) fn severity_to_str(severity: Option<lsp_types::DiagnosticSeverity>) -> &'static str {
1150 match severity {
1151 Some(lsp_types::DiagnosticSeverity::ERROR) => "ERROR",
1152 Some(lsp_types::DiagnosticSeverity::WARNING) => "WARNING",
1153 Some(lsp_types::DiagnosticSeverity::INFORMATION) => "INFO",
1154 Some(lsp_types::DiagnosticSeverity::HINT) => "HINT",
1155 Some(_) => "OTHER",
1156 None => "UNKNOWN",
1157 }
1158}
1159
1160pub(super) fn verification_stages_for_node(
1167 node: &SRBNNode,
1168) -> Vec<perspt_core::plugin::VerifierStage> {
1169 use perspt_core::plugin::VerifierStage;
1170 match node.node_class {
1171 perspt_core::types::NodeClass::Interface => {
1172 vec![VerifierStage::SyntaxCheck]
1173 }
1174 perspt_core::types::NodeClass::Implementation => {
1175 let mut stages = vec![VerifierStage::SyntaxCheck, VerifierStage::Build];
1176 let has_test_outputs = node.output_targets.iter().any(|p| {
1181 let s = p.to_string_lossy();
1182 let filename = p
1186 .file_name()
1187 .map(|f| f.to_string_lossy())
1188 .unwrap_or_default();
1189 s.contains("/tests/")
1190 || filename.starts_with("test_")
1191 || filename.contains(".test.")
1192 || filename.contains(".spec.")
1193 || filename.ends_with("_test.rs")
1194 || filename.ends_with("_test.py")
1195 || filename.ends_with("_tests.rs")
1196 || filename.ends_with("_tests.py")
1197 });
1198 if !node.contract.weighted_tests.is_empty() || has_test_outputs {
1199 stages.push(VerifierStage::Test);
1200 }
1201 stages
1202 }
1203 perspt_core::types::NodeClass::Integration => {
1204 vec![
1205 VerifierStage::SyntaxCheck,
1206 VerifierStage::Build,
1207 VerifierStage::Test,
1208 VerifierStage::Lint,
1209 ]
1210 }
1211 }
1212}