xchecker_engine/orchestrator/handle.rs
1//! Orchestrator façade for external consumers.
2//!
3//! This module provides a clean, stable API for external consumers (CLI, Kiro, MCP tools)
4//! to interact with the phase orchestrator without needing to know internal details.
5//!
6//! **Integration rule**: Outside `src/orchestrator/`, use `OrchestratorHandle`.
7//! Direct `PhaseOrchestrator` usage is reserved for tests and orchestrator internals.
8//!
9//! # Quick Start
10//!
11//! ```rust,no_run
12//! use xchecker_engine::orchestrator::OrchestratorHandle;
13//! use xchecker_engine::types::PhaseId;
14//!
15//! #[tokio::main]
16//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! // Using environment-based config discovery
18//! let mut handle = OrchestratorHandle::new("my-spec")?;
19//! handle.run_phase(PhaseId::Requirements).await?;
20//! Ok(())
21//! }
22//! ```
23
24use std::path::PathBuf;
25
26use anyhow::Result;
27
28use crate::config::{CliArgs, Config};
29use crate::error::{ConfigError, XCheckerError};
30use crate::receipt::ReceiptManager;
31use crate::spec_id::sanitize_spec_id;
32use crate::status::artifact::ArtifactManager;
33use crate::types::{PhaseId, StatusOutput};
34
35use super::{ExecutionResult, OrchestratorConfig, PhaseOrchestrator};
36
37/// The primary public API for embedding xchecker.
38///
39/// `OrchestratorHandle` provides a stable interface for creating specs and running
40/// phases programmatically. It is the canonical way to use xchecker outside of the CLI.
41///
42/// # Overview
43///
44/// Use `OrchestratorHandle` to:
45/// - Create and manage specs programmatically
46/// - Execute individual phases or the full workflow
47/// - Query spec status and artifacts
48/// - Configure execution options
49///
50/// # Construction
51///
52/// There are two ways to create an `OrchestratorHandle`:
53///
54/// - [`OrchestratorHandle::new`]: Uses environment-based config discovery (same as CLI)
55/// - [`OrchestratorHandle::from_config`]: Uses explicit configuration (deterministic)
56///
57/// # Threading
58///
59/// `OrchestratorHandle` is **NOT** guaranteed `Send` or `Sync` in 1.x.
60/// Treat as single-threaded; concurrent use is undefined behavior.
61/// This may be relaxed in future versions.
62///
63/// # Mutability
64///
65/// Methods that execute phases take `&mut self` to encode "sequential use only"
66/// semantics. This prevents accidental concurrent use at compile time.
67///
68/// # Sync vs Async
69///
70/// Public APIs are synchronous and manage their own async runtime internally.
71/// Tokio is an implementation detail not exposed to library consumers.
72///
73/// # Example
74///
75/// ```rust,no_run
76/// use xchecker_engine::orchestrator::OrchestratorHandle;
77/// use xchecker_engine::types::PhaseId;
78///
79/// #[tokio::main]
80/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
81/// // Using environment-based config discovery
82/// let mut handle = OrchestratorHandle::new("my-spec")?;
83///
84/// // Run a single phase
85/// handle.run_phase(PhaseId::Requirements).await?;
86///
87/// // Check status
88/// let status = handle.status()?;
89/// println!("Artifacts: {}", status.artifacts.len());
90///
91/// // Get the spec ID
92/// println!("Spec: {}", handle.spec_id());
93/// Ok(())
94/// }
95/// ```
96///
97/// # Using Explicit Configuration
98///
99/// ```rust,no_run
100/// use xchecker_engine::config::Config;
101/// use xchecker_engine::orchestrator::OrchestratorHandle;
102///
103/// // Create explicit config programmatically
104/// let config = Config::discover(&Default::default())?;
105/// let handle = OrchestratorHandle::from_config("my-spec", config)?;
106/// # Ok::<(), Box<dyn std::error::Error>>(())
107/// ```
108///
109/// # Error Handling
110///
111/// All methods return `Result` types. Errors are returned as [`XCheckerError`]
112/// which provides:
113/// - Rich context about what went wrong
114/// - Actionable suggestions for resolution
115/// - Mapping to CLI exit codes via [`XCheckerError::to_exit_code`]
116pub struct OrchestratorHandle {
117 orchestrator: PhaseOrchestrator,
118 config: OrchestratorConfig,
119 spec_id: String,
120}
121
122impl OrchestratorHandle {
123 /// Create a handle using environment-based config discovery.
124 ///
125 /// This uses the same discovery logic as the CLI:
126 /// - `XCHECKER_HOME` environment variable
127 /// - Upward search for `.xchecker/config.toml`
128 /// - Built-in defaults
129 ///
130 /// Acquires an exclusive lock on the spec directory.
131 ///
132 /// # Errors
133 ///
134 /// Returns error if:
135 /// - Configuration discovery fails
136 /// - Orchestrator creation fails
137 /// - Lock cannot be acquired
138 ///
139 /// # Example
140 ///
141 /// ```rust,no_run
142 /// use xchecker_engine::orchestrator::OrchestratorHandle;
143 ///
144 /// let handle = OrchestratorHandle::new("my-spec")?;
145 /// # Ok::<(), Box<dyn std::error::Error>>(())
146 /// ```
147 pub fn new(spec_id: &str) -> Result<Self, XCheckerError> {
148 // Use environment-based config discovery (same as CLI)
149 let config = Config::discover(&CliArgs::default())?;
150
151 Self::from_config_internal(spec_id, config, false)
152 }
153
154 /// Create a handle using explicit configuration.
155 ///
156 /// This does NOT probe the global environment or filesystem for config.
157 /// Use this when you need deterministic behavior independent of the
158 /// user's environment.
159 ///
160 /// # Errors
161 ///
162 /// Returns error if:
163 /// - Orchestrator creation fails
164 /// - Lock cannot be acquired
165 ///
166 /// # Example
167 ///
168 /// ```rust,no_run
169 /// use xchecker_engine::config::Config;
170 /// use xchecker_engine::orchestrator::OrchestratorHandle;
171 ///
172 /// // Create explicit config programmatically
173 /// let config = Config::discover(&Default::default())?;
174 /// let handle = OrchestratorHandle::from_config("my-spec", config)?;
175 /// # Ok::<(), Box<dyn std::error::Error>>(())
176 /// ```
177 pub fn from_config(spec_id: &str, config: Config) -> Result<Self, XCheckerError> {
178 Self::from_config_internal(spec_id, config, false)
179 }
180
181 /// Internal constructor that converts Config to OrchestratorConfig
182 fn from_config_internal(
183 spec_id: &str,
184 config: Config,
185 force: bool,
186 ) -> Result<Self, XCheckerError> {
187 // Sanitize spec ID to prevent path traversal and invalid characters
188 let sanitized_id = sanitize_spec_id(spec_id).map_err(|e| {
189 XCheckerError::Config(ConfigError::InvalidValue {
190 key: "spec_id".to_string(),
191 value: e.to_string(),
192 })
193 })?;
194
195 let redactor = crate::redaction::SecretRedactor::from_config(&config).map_err(
196 |e: anyhow::Error| {
197 XCheckerError::Config(ConfigError::InvalidValue {
198 key: "security".to_string(),
199 value: e.to_string(),
200 })
201 },
202 )?;
203
204 let orchestrator = if force {
205 PhaseOrchestrator::new_with_force(&sanitized_id, true)
206 } else {
207 PhaseOrchestrator::new(&sanitized_id)
208 }
209 .map_err(|e| {
210 XCheckerError::Config(crate::error::ConfigError::DiscoveryFailed {
211 reason: e.to_string(),
212 })
213 })?;
214
215 // Convert Config to OrchestratorConfig
216 let mut orch_config = OrchestratorConfig {
217 redactor: std::sync::Arc::new(redactor),
218 full_config: Some(config.clone()),
219 hooks: Some(config.hooks.clone()),
220 ..Default::default()
221 };
222
223 // Apply config values to orchestrator config
224 if let Some(packet_max_bytes) = config.defaults.packet_max_bytes {
225 orch_config
226 .config
227 .insert("packet_max_bytes".to_string(), packet_max_bytes.to_string());
228 }
229 if let Some(packet_max_lines) = config.defaults.packet_max_lines {
230 orch_config
231 .config
232 .insert("packet_max_lines".to_string(), packet_max_lines.to_string());
233 }
234 if let Some(max_turns) = config.defaults.max_turns {
235 orch_config
236 .config
237 .insert("max_turns".to_string(), max_turns.to_string());
238 }
239 if let Some(model) = &config.defaults.model {
240 orch_config
241 .config
242 .insert("model".to_string(), model.clone());
243 }
244 if let Some(output_format) = &config.defaults.output_format {
245 orch_config
246 .config
247 .insert("output_format".to_string(), output_format.clone());
248 }
249 if let Some(timeout) = config.defaults.phase_timeout {
250 orch_config
251 .config
252 .insert("phase_timeout".to_string(), timeout.to_string());
253 }
254 if let Some(stdout_cap_bytes) = config.defaults.stdout_cap_bytes {
255 orch_config
256 .config
257 .insert("stdout_cap_bytes".to_string(), stdout_cap_bytes.to_string());
258 }
259 if let Some(stderr_cap_bytes) = config.defaults.stderr_cap_bytes {
260 orch_config
261 .config
262 .insert("stderr_cap_bytes".to_string(), stderr_cap_bytes.to_string());
263 }
264 if let Some(lock_ttl_seconds) = config.defaults.lock_ttl_seconds {
265 orch_config
266 .config
267 .insert("lock_ttl_seconds".to_string(), lock_ttl_seconds.to_string());
268 }
269 if let Some(debug_packet) = config.defaults.debug_packet
270 && debug_packet
271 {
272 orch_config
273 .config
274 .insert("debug_packet".to_string(), "true".to_string());
275 }
276 if let Some(allow_links) = config.defaults.allow_links
277 && allow_links
278 {
279 orch_config
280 .config
281 .insert("allow_links".to_string(), "true".to_string());
282 }
283 if let Some(runner_mode) = &config.runner.mode {
284 orch_config
285 .config
286 .insert("runner_mode".to_string(), runner_mode.clone());
287 }
288 if let Some(runner_distro) = &config.runner.distro {
289 orch_config
290 .config
291 .insert("runner_distro".to_string(), runner_distro.clone());
292 }
293 if let Some(claude_path) = &config.runner.claude_path {
294 orch_config
295 .config
296 .insert("claude_path".to_string(), claude_path.clone());
297 }
298 if let Some(provider) = &config.llm.provider {
299 orch_config
300 .config
301 .insert("llm_provider".to_string(), provider.clone());
302 }
303 if let Some(fallback_provider) = &config.llm.fallback_provider {
304 orch_config.config.insert(
305 "llm_fallback_provider".to_string(),
306 fallback_provider.clone(),
307 );
308 }
309 if let Some(execution_strategy) = &config.llm.execution_strategy {
310 orch_config
311 .config
312 .insert("execution_strategy".to_string(), execution_strategy.clone());
313 }
314 if let Some(prompt_template) = &config.llm.prompt_template {
315 orch_config
316 .config
317 .insert("prompt_template".to_string(), prompt_template.clone());
318 }
319 if let Some(claude_config) = &config.llm.claude
320 && let Some(binary) = &claude_config.binary
321 {
322 orch_config
323 .config
324 .insert("llm_claude_binary".to_string(), binary.clone());
325 }
326 if let Some(gemini_config) = &config.llm.gemini {
327 if let Some(binary) = &gemini_config.binary {
328 orch_config
329 .config
330 .insert("llm_gemini_binary".to_string(), binary.clone());
331 }
332 if let Some(default_model) = &gemini_config.default_model {
333 orch_config.config.insert(
334 "llm_gemini_default_model".to_string(),
335 default_model.clone(),
336 );
337 }
338 }
339 orch_config.strict_validation = config.strict_validation();
340
341 // Copy selectors
342 orch_config.selectors = Some(config.selectors.clone());
343
344 Ok(Self {
345 orchestrator,
346 config: orch_config,
347 spec_id: sanitized_id,
348 })
349 }
350
351 /// Create a handle with force flag for lock override.
352 ///
353 /// Use with caution: forcing lock override can lead to race conditions if another
354 /// process is actively working on the spec.
355 ///
356 /// # Errors
357 ///
358 /// Returns error if orchestrator creation fails.
359 pub fn with_force(spec_id: &str, force: bool) -> Result<Self, XCheckerError> {
360 let config = Config::discover(&CliArgs::default())?;
361
362 Self::from_config_internal(spec_id, config, force)
363 }
364
365 /// Create a handle with custom OrchestratorConfig and force flag.
366 ///
367 /// This is used by the CLI when it needs to pass specific orchestrator
368 /// configuration options.
369 ///
370 /// # Errors
371 ///
372 /// Returns error if orchestrator creation fails.
373 pub fn with_config_and_force(
374 spec_id: &str,
375 config: OrchestratorConfig,
376 force: bool,
377 ) -> Result<Self, XCheckerError> {
378 // Sanitize spec ID
379 let sanitized_id = sanitize_spec_id(spec_id).map_err(|e| {
380 XCheckerError::Config(ConfigError::InvalidValue {
381 key: "spec_id".to_string(),
382 value: e.to_string(),
383 })
384 })?;
385
386 let orchestrator = if force {
387 PhaseOrchestrator::new_with_force(&sanitized_id, true)
388 } else {
389 PhaseOrchestrator::new(&sanitized_id)
390 }
391 .map_err(|e| {
392 XCheckerError::Config(crate::error::ConfigError::DiscoveryFailed {
393 reason: e.to_string(),
394 })
395 })?;
396
397 Ok(Self {
398 orchestrator,
399 config,
400 spec_id: sanitized_id,
401 })
402 }
403
404 /// Create a read-only handle for status inspection.
405 ///
406 /// Does not acquire locks, allowing inspection while another process
407 /// is actively working on the spec.
408 ///
409 /// # Errors
410 ///
411 /// Returns error if orchestrator creation fails.
412 pub fn readonly(spec_id: &str) -> Result<Self, XCheckerError> {
413 // Sanitize spec ID
414 let sanitized_id = sanitize_spec_id(spec_id).map_err(|e| {
415 XCheckerError::Config(ConfigError::InvalidValue {
416 key: "spec_id".to_string(),
417 value: e.to_string(),
418 })
419 })?;
420
421 let orchestrator = PhaseOrchestrator::new_readonly(&sanitized_id).map_err(|e| {
422 XCheckerError::Config(crate::error::ConfigError::DiscoveryFailed {
423 reason: e.to_string(),
424 })
425 })?;
426
427 let config = OrchestratorConfig::default();
428
429 Ok(Self {
430 orchestrator,
431 config,
432 spec_id: sanitized_id,
433 })
434 }
435
436 /// Execute a single phase.
437 ///
438 /// Behavior matches the CLI `xchecker resume --phase <phase>` command.
439 /// Takes `&mut self` to enforce sequential use.
440 ///
441 /// # Errors
442 ///
443 /// Returns error if transition is invalid or execution fails.
444 ///
445 /// # Example
446 ///
447 /// ```rust,no_run
448 /// use xchecker_engine::orchestrator::OrchestratorHandle;
449 /// use xchecker_engine::types::PhaseId;
450 ///
451 /// # #[tokio::main]
452 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
453 /// let mut handle = OrchestratorHandle::new("my-spec")?;
454 /// handle.run_phase(PhaseId::Requirements).await?;
455 /// # Ok(())
456 /// # }
457 /// ```
458 pub async fn run_phase(&mut self, phase: PhaseId) -> Result<ExecutionResult> {
459 self.orchestrator
460 .resume_from_phase(phase, &self.config)
461 .await
462 }
463
464 /// Execute all phases in sequence.
465 ///
466 /// Stops on first failure. Behavior matches the CLI `xchecker spec` command.
467 /// Takes `&mut self` to enforce sequential use.
468 ///
469 /// # Errors
470 ///
471 /// Returns error if any phase fails.
472 ///
473 /// # Example
474 ///
475 /// ```rust,no_run
476 /// use xchecker_engine::orchestrator::OrchestratorHandle;
477 ///
478 /// # #[tokio::main]
479 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
480 /// let mut handle = OrchestratorHandle::new("my-spec")?;
481 /// handle.run_all().await?;
482 /// # Ok(())
483 /// # }
484 /// ```
485 pub async fn run_all(&mut self) -> Result<ExecutionResult> {
486 // Execute phases in sequence: Requirements -> Design -> Tasks
487 // (Review, Fixup, Final are optional/advanced phases)
488 let phases = [PhaseId::Requirements, PhaseId::Design, PhaseId::Tasks];
489
490 let mut last_result = None;
491 for phase in phases {
492 let result = self
493 .orchestrator
494 .resume_from_phase(phase, &self.config)
495 .await?;
496
497 if !result.success {
498 return Ok(result);
499 }
500 last_result = Some(result);
501 }
502
503 // Return the last successful result
504 last_result.ok_or_else(|| anyhow::anyhow!("No phases executed"))
505 }
506
507 /// Get the current spec status.
508 ///
509 /// Returns `StatusOutput` which is part of the stable public API.
510 ///
511 /// # Errors
512 ///
513 /// Returns error if status generation fails.
514 pub fn status(&self) -> Result<StatusOutput, XCheckerError> {
515 use std::collections::BTreeMap;
516
517 let mut effective_config: BTreeMap<String, (String, String)> = self
518 .config
519 .full_config
520 .as_ref()
521 .map(|config| config.effective_config().into_iter().collect())
522 .unwrap_or_default();
523
524 // Merge any programmatic overrides (e.g., set_config) without losing source attribution.
525 for (key, value) in &self.config.config {
526 let override_needed = match effective_config.get(key) {
527 Some((existing_value, _)) => existing_value != value,
528 None => true,
529 };
530 if override_needed {
531 effective_config.insert(key.clone(), (value.clone(), "programmatic".to_string()));
532 }
533 }
534
535 crate::status::status::StatusManager::generate_status_internal(
536 self.orchestrator.artifact_manager(),
537 self.orchestrator.receipt_manager(),
538 effective_config,
539 None,
540 None,
541 Some(&self.config.redactor),
542 )
543 .map_err(|e| {
544 XCheckerError::Config(ConfigError::DiscoveryFailed {
545 reason: format!("Failed to generate status: {e}"),
546 })
547 })
548 }
549
550 /// Get the path to the most recent receipt.
551 ///
552 /// Returns `None` if no receipts have been written.
553 #[must_use]
554 pub fn last_receipt_path(&self) -> Option<PathBuf> {
555 // Check each phase in reverse order to find the most recent receipt
556 let phases = [
557 PhaseId::Final,
558 PhaseId::Fixup,
559 PhaseId::Review,
560 PhaseId::Tasks,
561 PhaseId::Design,
562 PhaseId::Requirements,
563 ];
564
565 for phase in &phases {
566 if let Ok(Some(_receipt)) = self
567 .orchestrator
568 .receipt_manager()
569 .read_latest_receipt(*phase)
570 {
571 // Construct the receipt path from the receipt manager's base path
572 let base_path = self.orchestrator.artifact_manager().base_path();
573 let receipts_dir = base_path.join("receipts");
574
575 // Find the most recent receipt file for this phase
576 if let Ok(entries) = std::fs::read_dir(&receipts_dir) {
577 let phase_prefix = format!("{}-", phase.as_str());
578 let mut receipt_files: Vec<_> = entries
579 .filter_map(|e| e.ok())
580 .filter(|e| e.file_name().to_string_lossy().starts_with(&phase_prefix))
581 .collect();
582
583 // Sort by name (timestamp-based) to get the most recent
584 receipt_files.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
585
586 if let Some(entry) = receipt_files.first() {
587 return Some(entry.path());
588 }
589 }
590 }
591 }
592
593 None
594 }
595
596 /// Get the spec ID this handle operates on.
597 #[must_use]
598 pub fn spec_id(&self) -> &str {
599 &self.spec_id
600 }
601
602 /// Check if a phase can be run.
603 ///
604 /// Validates that all dependencies are satisfied and have successful receipts.
605 ///
606 /// # Returns
607 ///
608 /// `true` if the phase can be executed, `false` otherwise.
609 pub fn can_run_phase(&self, phase: PhaseId) -> Result<bool> {
610 self.orchestrator.can_resume_from_phase_public(phase)
611 }
612
613 /// Get the current phase state.
614 ///
615 /// Returns the last successfully completed phase, or `None` if no phases
616 /// have been completed.
617 pub fn current_phase(&self) -> Result<Option<PhaseId>> {
618 self.orchestrator.get_current_phase_state()
619 }
620
621 /// Get legal next phases from current state.
622 ///
623 /// Returns a list of phases that can be validly executed based on
624 /// the current workflow state.
625 pub fn legal_next_phases(&self) -> Result<Vec<PhaseId>> {
626 let current = self.current_phase()?;
627 Ok(match current {
628 None => vec![PhaseId::Requirements],
629 Some(PhaseId::Requirements) => vec![PhaseId::Requirements, PhaseId::Design],
630 Some(PhaseId::Design) => vec![PhaseId::Design, PhaseId::Tasks],
631 Some(PhaseId::Tasks) => vec![PhaseId::Tasks, PhaseId::Review, PhaseId::Final],
632 Some(PhaseId::Review) => vec![PhaseId::Review, PhaseId::Fixup, PhaseId::Final],
633 Some(PhaseId::Fixup) => vec![PhaseId::Fixup, PhaseId::Final],
634 Some(PhaseId::Final) => vec![PhaseId::Final],
635 })
636 }
637
638 /// Set a configuration option.
639 ///
640 /// Common keys include:
641 /// - `model`: LLM model to use
642 /// - `phase_timeout`: Timeout in seconds
643 /// - `apply_fixups`: Whether to apply fixups or preview
644 pub fn set_config(&mut self, key: &str, value: &str) {
645 self.config
646 .config
647 .insert(key.to_string(), value.to_string());
648 }
649
650 /// Get a configuration option.
651 ///
652 /// Returns `None` if the key is not set.
653 #[must_use]
654 pub fn get_config(&self, key: &str) -> Option<&String> {
655 self.config.config.get(key)
656 }
657
658 /// Enable or disable dry-run mode.
659 ///
660 /// In dry-run mode, phases are simulated without calling the LLM.
661 pub fn set_dry_run(&mut self, dry_run: bool) {
662 self.config.dry_run = dry_run;
663 }
664
665 /// Get the current orchestrator configuration.
666 ///
667 /// Returns a reference to the configuration used for phase execution.
668 #[must_use]
669 pub fn orchestrator_config(&self) -> &OrchestratorConfig {
670 &self.config
671 }
672
673 /// Access the artifact manager for status queries.
674 ///
675 /// Use this for read-only operations like checking phase completion,
676 /// listing artifacts, or getting the base path.
677 #[must_use]
678 #[doc(hidden)]
679 pub fn artifact_manager(&self) -> &ArtifactManager {
680 self.orchestrator.artifact_manager()
681 }
682
683 /// Access the receipt manager for status queries.
684 ///
685 /// Use this for read-only operations like listing receipts or
686 /// getting receipt metadata.
687 #[must_use]
688 #[doc(hidden)]
689 pub fn receipt_manager(&self) -> &ReceiptManager {
690 self.orchestrator.receipt_manager()
691 }
692
693 /// Get a reference to the underlying orchestrator.
694 ///
695 /// This is primarily for interop with APIs that require `&PhaseOrchestrator`,
696 /// such as `StatusManager::generate_status_from_orchestrator`.
697 ///
698 /// Prefer using the high-level methods on `OrchestratorHandle` when possible.
699 #[must_use]
700 #[doc(hidden)]
701 pub fn as_orchestrator(&self) -> &PhaseOrchestrator {
702 &self.orchestrator
703 }
704}
705
706// Implement SpecDataProvider trait for gate module
707impl xchecker_gate::SpecDataProvider for &OrchestratorHandle {
708 fn base_path(&self) -> &std::path::Path {
709 self.orchestrator
710 .artifact_manager()
711 .base_path()
712 .as_std_path()
713 }
714
715 fn spec_id(&self) -> &str {
716 &self.spec_id
717 }
718
719 fn receipt_manager(&self) -> &xchecker_receipt::ReceiptManager {
720 self.orchestrator.receipt_manager()
721 }
722
723 fn phase_completed(&self, phase: xchecker_utils::types::PhaseId) -> bool {
724 self.orchestrator.artifact_manager().phase_completed(phase)
725 }
726
727 fn pending_fixups_result(&self) -> xchecker_gate::PendingFixupsResult {
728 use crate::fixup::pending_fixups_result_from_handle;
729 pending_fixups_result_from_handle(self)
730 }
731}