Skip to main content

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}