debtmap/
effects.rs

1//! Effect type aliases and helpers for debtmap analysis.
2//!
3//! This module provides type aliases that integrate stillwater's effect system
4//! with debtmap's environment and error types. Using these aliases:
5//!
6//! - Reduces boilerplate in function signatures
7//! - Centralizes the environment and error types
8//! - Makes it easy to refactor if types change
9//!
10//! # Effect vs Validation
11//!
12//! - **Effect**: Represents a computation that may perform I/O and may fail.
13//!   Use for operations like reading files or loading coverage data.
14//!
15//! - **Validation**: Represents a validation check that accumulates ALL errors
16//!   instead of failing at the first one. Use for configuration validation,
17//!   input checking, and anywhere you want comprehensive error reporting.
18//!
19//! # Reader Pattern (Spec 199)
20//!
21//! This module also provides **Reader pattern** helpers using stillwater 0.11.0's
22//! zero-cost `ask`, `asks`, and `local` primitives. The Reader pattern eliminates
23//! config parameter threading by making configuration available through the
24//! environment.
25//!
26//! ## Reader Pattern Benefits
27//!
28//! **Before (parameter threading):**
29//! ```rust,ignore
30//! fn analyze(ast: &Ast, config: &Config) -> Metrics {
31//!     calculate_complexity(ast, &config.thresholds)
32//! }
33//! ```
34//!
35//! **After (Reader pattern):**
36//! ```rust,ignore
37//! use debtmap::effects::asks_config;
38//!
39//! fn analyze_effect<Env>(ast: Ast) -> impl Effect<...>
40//! where Env: AnalysisEnv + Clone + Send + Sync
41//! {
42//!     asks_config(move |config| calculate_complexity(&ast, &config.thresholds))
43//! }
44//! ```
45//!
46//! ## Available Reader Helpers
47//!
48//! - [`asks_config`]: Access the full config via closure
49//! - [`asks_thresholds`]: Access thresholds config section
50//! - [`asks_scoring`]: Access scoring weights config section
51//! - [`asks_entropy`]: Access entropy config section
52//! - [`local_with_config`]: Run effect with modified config (temporary override)
53//!
54//! # Example: Using Effects
55//!
56//! ```rust,ignore
57//! use debtmap::effects::AnalysisEffect;
58//! use debtmap::env::AnalysisEnv;
59//! use stillwater::Effect;
60//!
61//! fn read_source(path: PathBuf) -> AnalysisEffect<String> {
62//!     Effect::from_fn(move |env: &dyn AnalysisEnv| {
63//!         env.file_system()
64//!             .read_to_string(&path)
65//!             .map_err(Into::into)
66//!     })
67//! }
68//! ```
69//!
70//! # Example: Using Validation
71//!
72//! ```rust,ignore
73//! use debtmap::effects::{AnalysisValidation, validation_success, validation_failure};
74//!
75//! fn validate_thresholds(complexity: u32, lines: usize) -> AnalysisValidation<()> {
76//!     let v1 = if complexity <= 50 {
77//!         validation_success(())
78//!     } else {
79//!         validation_failure(AnalysisError::validation("Complexity too high"))
80//!     };
81//!
82//!     let v2 = if lines <= 1000 {
83//!         validation_success(())
84//!     } else {
85//!         validation_failure(AnalysisError::validation("File too long"))
86//!     };
87//!
88//!     // Combine validations - collects ALL errors
89//!     v1.and(v2)
90//! }
91//! ```
92//!
93//! # Example: Using Reader Pattern
94//!
95//! ```rust,ignore
96//! use debtmap::effects::{asks_config, asks_thresholds, local_with_config};
97//! use debtmap::env::AnalysisEnv;
98//! use stillwater::Effect;
99//!
100//! // Query config via closure
101//! fn get_complexity_threshold<Env>() -> impl Effect<Output = Option<u32>, Error = AnalysisError, Env = Env>
102//! where
103//!     Env: AnalysisEnv + Clone + Send + Sync,
104//! {
105//!     asks_config(|config| config.thresholds.as_ref().and_then(|t| t.complexity))
106//! }
107//!
108//! // Use temporary config override
109//! fn analyze_strict<Env>(path: PathBuf) -> impl Effect<...>
110//! where
111//!     Env: AnalysisEnv + Clone + Send + Sync,
112//! {
113//!     local_with_config(
114//!         |config| {
115//!             let mut strict = config.clone();
116//!             // Apply stricter thresholds
117//!             strict
118//!         },
119//!         analyze_file_effect(path)
120//!     )
121//! }
122//! ```
123
124use crate::config::DebtmapConfig;
125use crate::env::{AnalysisEnv, RealEnv};
126use crate::errors::{errors_to_anyhow, AnalysisError};
127use stillwater::effect::prelude::*;
128use stillwater::{BoxedEffect, NonEmptyVec, Validation};
129
130/// Error collection type for validation accumulation.
131///
132/// This type holds multiple errors during validation, enabling comprehensive
133/// error reporting instead of failing at the first error.
134pub type AnalysisErrors = NonEmptyVec<AnalysisError>;
135
136/// Effect type for debtmap analysis operations.
137///
138/// This type alias parameterizes stillwater's Effect with:
139/// - Success type `T` (the computation result)
140/// - Error type `AnalysisError` (our unified error type)
141/// - Environment type `RealEnv` (production I/O capabilities)
142///
143/// # Usage
144///
145/// ```rust,ignore
146/// fn analyze_file(path: PathBuf) -> AnalysisEffect<FileMetrics> {
147///     Effect::from_fn(move |env| {
148///         let content = env.file_system().read_to_string(&path)?;
149///         let metrics = compute_metrics(&content);
150///         Ok(metrics)
151///     })
152/// }
153/// ```
154pub type AnalysisEffect<T> = BoxedEffect<T, AnalysisError, RealEnv>;
155
156/// Validation type for debtmap validations.
157///
158/// This type alias uses stillwater's Validation with:
159/// - Success type `T` (the validated value)
160/// - Error type `NonEmptyVec<AnalysisError>` (accumulated errors)
161///
162/// Unlike Result, Validation accumulates ALL errors instead of short-circuiting
163/// at the first failure. This is useful for:
164/// - Configuration validation (report all issues at once)
165/// - Input validation (show all problems to the user)
166/// - Analysis validation (collect all findings)
167///
168/// # Usage
169///
170/// ```rust
171/// use debtmap::effects::{AnalysisValidation, validation_success, validation_failure};
172/// use debtmap::errors::AnalysisError;
173///
174/// fn validate_name(name: &str) -> AnalysisValidation<String> {
175///     if name.is_empty() {
176///         validation_failure(AnalysisError::validation("Name cannot be empty"))
177///     } else {
178///         validation_success(name.to_string())
179///     }
180/// }
181/// ```
182pub type AnalysisValidation<T> = Validation<T, AnalysisErrors>;
183
184/// Create a successful validation result.
185///
186/// # Example
187///
188/// ```rust
189/// use debtmap::effects::validation_success;
190///
191/// let v: debtmap::effects::AnalysisValidation<i32> = validation_success(42);
192/// assert!(v.is_success());
193/// ```
194pub fn validation_success<T>(value: T) -> AnalysisValidation<T> {
195    Validation::Success(value)
196}
197
198/// Create a failed validation result with a single error.
199///
200/// # Example
201///
202/// ```rust
203/// use debtmap::effects::validation_failure;
204/// use debtmap::errors::AnalysisError;
205///
206/// let v: debtmap::effects::AnalysisValidation<i32> =
207///     validation_failure(AnalysisError::validation("Invalid input"));
208/// assert!(v.is_failure());
209/// ```
210pub fn validation_failure<T>(error: AnalysisError) -> AnalysisValidation<T> {
211    Validation::Failure(NonEmptyVec::new(error, Vec::new()))
212}
213
214/// Create a failed validation result with multiple errors.
215///
216/// # Panics
217///
218/// Panics if the errors vector is empty. Use `validation_failure` for
219/// single errors or ensure the vector is non-empty.
220///
221/// # Example
222///
223/// ```rust
224/// use debtmap::effects::validation_failures;
225/// use debtmap::errors::AnalysisError;
226///
227/// let errors = vec![
228///     AnalysisError::validation("Error 1"),
229///     AnalysisError::validation("Error 2"),
230/// ];
231/// let v: debtmap::effects::AnalysisValidation<i32> = validation_failures(errors);
232/// ```
233pub fn validation_failures<T>(errors: Vec<AnalysisError>) -> AnalysisValidation<T> {
234    let nev =
235        NonEmptyVec::from_vec(errors).expect("validation_failures requires at least one error");
236    Validation::Failure(nev)
237}
238
239/// Create an effect from a pure value (no I/O).
240///
241/// This is useful for wrapping pure computations in the effect system.
242///
243/// # Example
244///
245/// ```rust,ignore
246/// let effect = effect_pure(42);
247/// assert_eq!(effect.run(&env).unwrap(), 42);
248/// ```
249pub fn effect_pure<T: Send + 'static>(value: T) -> AnalysisEffect<T> {
250    pure(value).boxed()
251}
252
253/// Create an effect from an error.
254///
255/// This is useful for creating failing effects without needing I/O.
256///
257/// # Example
258///
259/// ```rust,ignore
260/// let effect: AnalysisEffect<i32> = effect_fail(AnalysisError::validation("bad input"));
261/// assert!(effect.run(&env).is_err());
262/// ```
263pub fn effect_fail<T: Send + 'static>(error: AnalysisError) -> AnalysisEffect<T> {
264    fail(error).boxed()
265}
266
267/// Create an effect from a synchronous function.
268///
269/// The function receives the environment and should return a Result.
270///
271/// # Example
272///
273/// ```rust,ignore
274/// fn read_config() -> AnalysisEffect<Config> {
275///     effect_from_fn(|env| {
276///         let content = env.file_system().read_to_string(Path::new("config.toml"))?;
277///         parse_config(&content).map_err(Into::into)
278///     })
279/// }
280/// ```
281pub fn effect_from_fn<T, F>(f: F) -> AnalysisEffect<T>
282where
283    T: Send + 'static,
284    F: FnOnce(&RealEnv) -> Result<T, AnalysisError> + Send + 'static,
285{
286    from_fn(f).boxed()
287}
288
289/// Run an effect and convert the result to anyhow::Result for backwards compatibility.
290///
291/// This function bridges the new effect system with existing code that uses
292/// anyhow::Result. Use this at the boundaries of your code where you need
293/// to integrate with existing APIs.
294///
295/// Note: This uses tokio's block_on to run the async effect synchronously.
296/// For better performance in async contexts, use `run_effect_async` instead.
297///
298/// # Example
299///
300/// ```rust,ignore
301/// // Old code using anyhow
302/// fn old_api() -> anyhow::Result<Metrics> {
303///     let config = DebtmapConfig::default();
304///     run_effect(analyze_effect(), config)
305/// }
306/// ```
307pub fn run_effect<T: Send + 'static>(
308    effect: AnalysisEffect<T>,
309    config: DebtmapConfig,
310) -> anyhow::Result<T> {
311    let env = RealEnv::new(config);
312    // Use tokio runtime to block on the async effect
313    let rt = tokio::runtime::Builder::new_current_thread()
314        .enable_all()
315        .build()
316        .map_err(|e| anyhow::anyhow!("Failed to create tokio runtime: {}", e))?;
317    rt.block_on(effect.run(&env)).map_err(Into::into)
318}
319
320/// Run an effect with a custom environment.
321///
322/// This is useful when you have an existing environment or need custom
323/// I/O implementations.
324///
325/// Note: This uses tokio's block_on to run the async effect synchronously.
326pub fn run_effect_with_env<T: Send + 'static, E: AnalysisEnv + Sync + 'static>(
327    effect: BoxedEffect<T, AnalysisError, E>,
328    env: &E,
329) -> anyhow::Result<T> {
330    let rt = tokio::runtime::Builder::new_current_thread()
331        .enable_all()
332        .build()
333        .map_err(|e| anyhow::anyhow!("Failed to create tokio runtime: {}", e))?;
334    rt.block_on(effect.run(env)).map_err(Into::into)
335}
336
337/// Run an effect asynchronously.
338///
339/// This is the preferred method when you're already in an async context.
340pub async fn run_effect_async<T: Send + 'static>(
341    effect: AnalysisEffect<T>,
342    config: DebtmapConfig,
343) -> anyhow::Result<T> {
344    let env = RealEnv::new(config);
345    effect.run(&env).await.map_err(Into::into)
346}
347
348/// Convert a Validation result to anyhow::Result for backwards compatibility.
349///
350/// If the validation failed with multiple errors, they are formatted as a list.
351///
352/// # Example
353///
354/// ```rust
355/// use debtmap::effects::{run_validation, validation_success, validation_failure};
356/// use debtmap::errors::AnalysisError;
357///
358/// let success = validation_success(42);
359/// assert_eq!(run_validation(success).unwrap(), 42);
360///
361/// let failure: debtmap::effects::AnalysisValidation<i32> =
362///     validation_failure(AnalysisError::validation("bad input"));
363/// assert!(run_validation(failure).is_err());
364/// ```
365pub fn run_validation<T>(validation: AnalysisValidation<T>) -> anyhow::Result<T> {
366    match validation {
367        Validation::Success(value) => Ok(value),
368        Validation::Failure(errors) => Err(errors_to_anyhow(errors.into_vec())),
369    }
370}
371
372/// Combine multiple validations, accumulating all errors.
373///
374/// This is the core of error accumulation - if any validation fails,
375/// all errors are collected. If all succeed, the results are collected.
376///
377/// # Example
378///
379/// ```rust
380/// use debtmap::effects::{combine_validations, validation_success, validation_failure};
381/// use debtmap::errors::AnalysisError;
382///
383/// let validations = vec![
384///     validation_success(1),
385///     validation_failure(AnalysisError::validation("error 1")),
386///     validation_success(3),
387///     validation_failure(AnalysisError::validation("error 2")),
388/// ];
389///
390/// let result = combine_validations(validations);
391/// // Result contains BOTH errors, not just the first one
392/// ```
393pub fn combine_validations<T>(validations: Vec<AnalysisValidation<T>>) -> AnalysisValidation<Vec<T>>
394where
395    T: Clone,
396{
397    let mut successes = Vec::new();
398    let mut failures: Vec<AnalysisError> = Vec::new();
399
400    for v in validations {
401        match v {
402            Validation::Success(value) => successes.push(value),
403            Validation::Failure(errors) => {
404                for err in errors {
405                    failures.push(err);
406                }
407            }
408        }
409    }
410
411    if failures.is_empty() {
412        Validation::Success(successes)
413    } else {
414        Validation::Failure(NonEmptyVec::from_vec(failures).expect("failures cannot be empty here"))
415    }
416}
417
418/// Map a function over a validation's success value.
419///
420/// If the validation is successful, applies the function.
421/// If it failed, passes through the errors unchanged.
422pub fn validation_map<T, U, F>(validation: AnalysisValidation<T>, f: F) -> AnalysisValidation<U>
423where
424    F: FnOnce(T) -> U,
425{
426    match validation {
427        Validation::Success(value) => Validation::Success(f(value)),
428        Validation::Failure(errors) => Validation::Failure(errors),
429    }
430}
431
432// =============================================================================
433// Reader Pattern Helpers (Spec 199)
434// =============================================================================
435//
436// These helpers use stillwater 0.11.0's zero-cost Reader primitives to provide
437// config access without parameter threading.
438
439use crate::config::{EntropyConfig, ScoringWeights, ThresholdsConfig};
440use stillwater::Effect;
441
442use std::sync::Arc;
443
444/// A wrapper type that makes a shared function callable via `Fn` traits.
445///
446/// This wrapper holds an `Arc<F>` and implements `Fn` by cloning the `Arc`
447/// on each call, allowing the function to be called multiple times.
448#[derive(Clone)]
449pub struct SharedFn<F>(Arc<F>);
450
451impl<F> SharedFn<F> {
452    fn new(f: F) -> Self {
453        Self(Arc::new(f))
454    }
455}
456
457/// Query config through closure - the core Reader pattern primitive.
458///
459/// This function creates an effect that queries the environment's config
460/// using a provided closure. The closure receives a reference to the
461/// [`DebtmapConfig`] and returns any value.
462///
463/// Uses `Arc` internally to allow the closure to be called multiple times.
464///
465/// # Type Parameters
466///
467/// - `U`: The return type of the query
468/// - `Env`: The environment type (must implement [`AnalysisEnv`])
469/// - `F`: The query function type
470///
471/// # Example
472///
473/// ```rust,ignore
474/// use debtmap::effects::asks_config;
475///
476/// // Get ignore patterns from config
477/// fn get_ignore_patterns<Env>() -> impl Effect<Output = Vec<String>, Error = AnalysisError, Env = Env>
478/// where
479///     Env: AnalysisEnv + Clone + Send + Sync,
480/// {
481///     asks_config(|config| config.get_ignore_patterns())
482/// }
483///
484/// // Get complexity threshold
485/// fn get_complexity_threshold<Env>() -> impl Effect<Output = Option<u32>, Error = AnalysisError, Env = Env>
486/// where
487///     Env: AnalysisEnv + Clone + Send + Sync,
488/// {
489///     asks_config(|config| config.thresholds.as_ref().and_then(|t| t.complexity))
490/// }
491/// ```
492pub fn asks_config<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
493where
494    U: Send + 'static,
495    Env: AnalysisEnv + Clone + Send + Sync + 'static,
496    F: Fn(&DebtmapConfig) -> U + Send + Sync + 'static,
497{
498    let shared = SharedFn::new(f);
499    stillwater::asks(move |env: &Env| (shared.0)(env.config()))
500}
501
502/// Query thresholds config section.
503///
504/// Convenience helper for accessing the thresholds configuration.
505/// Returns `None` if thresholds are not configured.
506///
507/// # Example
508///
509/// ```rust,ignore
510/// use debtmap::effects::asks_thresholds;
511///
512/// fn get_max_file_length<Env>() -> impl Effect<Output = Option<usize>, Error = AnalysisError, Env = Env>
513/// where
514///     Env: AnalysisEnv + Clone + Send + Sync,
515/// {
516///     asks_thresholds(|thresholds| thresholds.and_then(|t| t.max_file_length))
517/// }
518/// ```
519pub fn asks_thresholds<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
520where
521    U: Send + 'static,
522    Env: AnalysisEnv + Clone + Send + Sync + 'static,
523    F: Fn(Option<&ThresholdsConfig>) -> U + Send + Sync + 'static,
524{
525    let shared = SharedFn::new(f);
526    stillwater::asks(move |env: &Env| (shared.0)(env.config().thresholds.as_ref()))
527}
528
529/// Query scoring weights config section.
530///
531/// Convenience helper for accessing the scoring weights configuration.
532/// Returns `None` if scoring weights are not configured.
533///
534/// # Example
535///
536/// ```rust,ignore
537/// use debtmap::effects::asks_scoring;
538///
539/// fn get_coverage_weight<Env>() -> impl Effect<Output = f64, Error = AnalysisError, Env = Env>
540/// where
541///     Env: AnalysisEnv + Clone + Send + Sync,
542/// {
543///     asks_scoring(|scoring| scoring.map(|s| s.coverage).unwrap_or(0.5))
544/// }
545/// ```
546pub fn asks_scoring<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
547where
548    U: Send + 'static,
549    Env: AnalysisEnv + Clone + Send + Sync + 'static,
550    F: Fn(Option<&ScoringWeights>) -> U + Send + Sync + 'static,
551{
552    let shared = SharedFn::new(f);
553    stillwater::asks(move |env: &Env| (shared.0)(env.config().scoring.as_ref()))
554}
555
556/// Query entropy config section.
557///
558/// Convenience helper for accessing the entropy configuration.
559/// Returns `None` if entropy config is not set.
560///
561/// # Example
562///
563/// ```rust,ignore
564/// use debtmap::effects::asks_entropy;
565///
566/// fn is_entropy_enabled<Env>() -> impl Effect<Output = bool, Error = AnalysisError, Env = Env>
567/// where
568///     Env: AnalysisEnv + Clone + Send + Sync,
569/// {
570///     asks_entropy(|entropy| entropy.map(|e| e.enabled).unwrap_or(true))
571/// }
572/// ```
573pub fn asks_entropy<U, Env, F>(f: F) -> impl Effect<Output = U, Error = AnalysisError, Env = Env>
574where
575    U: Send + 'static,
576    Env: AnalysisEnv + Clone + Send + Sync + 'static,
577    F: Fn(Option<&EntropyConfig>) -> U + Send + Sync + 'static,
578{
579    let shared = SharedFn::new(f);
580    stillwater::asks(move |env: &Env| (shared.0)(env.config().entropy.as_ref()))
581}
582
583/// Run an effect with a temporarily modified config.
584///
585/// This is the Reader pattern's `local` operation - it allows running an
586/// inner effect with a modified environment. The modification is only
587/// visible to the inner effect; after it completes, the original
588/// environment is restored.
589///
590/// This is useful for:
591/// - **Strict mode**: Running analysis with stricter thresholds
592/// - **Custom thresholds**: Temporarily overriding specific settings
593/// - **Feature flags**: Temporarily enabling/disabling features
594///
595/// # Type Parameters
596///
597/// - `Inner`: The inner effect type
598/// - `F`: The environment transformation function
599/// - `Env`: The environment type
600///
601/// # Example
602///
603/// ```rust,ignore
604/// use debtmap::effects::local_with_config;
605///
606/// // Run analysis in strict mode (lower complexity threshold)
607/// fn analyze_strict<Env>(
608///     path: PathBuf,
609/// ) -> impl Effect<Output = FileAnalysis, Error = AnalysisError, Env = Env>
610/// where
611///     Env: AnalysisEnv + Clone + Send + Sync,
612/// {
613///     local_with_config(
614///         |config| {
615///             let mut strict = config.clone();
616///             if let Some(ref mut thresholds) = strict.thresholds {
617///                 // Reduce complexity threshold by half
618///                 if let Some(complexity) = thresholds.complexity {
619///                     thresholds.complexity = Some(complexity / 2);
620///                 }
621///             }
622///             strict
623///         },
624///         analyze_file_effect(path)
625///     )
626/// }
627///
628/// // Temporarily disable entropy-based scoring
629/// fn analyze_without_entropy<Env>(
630///     inner: impl Effect<Output = T, Error = AnalysisError, Env = Env>
631/// ) -> impl Effect<Output = T, Error = AnalysisError, Env = Env>
632/// where
633///     Env: AnalysisEnv + Clone + Send + Sync,
634/// {
635///     local_with_config(
636///         |config| {
637///             let mut modified = config.clone();
638///             if let Some(ref mut entropy) = modified.entropy {
639///                 entropy.enabled = false;
640///             }
641///             modified
642///         },
643///         inner
644///     )
645/// }
646/// ```
647pub fn local_with_config<Inner, F, Env>(
648    f: F,
649    inner: Inner,
650) -> impl Effect<Output = Inner::Output, Error = Inner::Error, Env = Env>
651where
652    Env: AnalysisEnv + Clone + Send + Sync + 'static,
653    F: Fn(&DebtmapConfig) -> DebtmapConfig + Send + Sync + 'static,
654    Inner: Effect<Env = Env>,
655{
656    let shared = SharedFn::new(f);
657    stillwater::local(
658        move |env: &Env| {
659            let new_config = (shared.0)(env.config());
660            env.clone().with_config(new_config)
661        },
662        inner,
663    )
664}
665
666/// Query the entire environment.
667///
668/// This is a low-level helper that returns a clone of the entire environment.
669/// Prefer using [`asks_config`] or the specific query helpers when you only
670/// need config access.
671///
672/// # Example
673///
674/// ```rust,ignore
675/// use debtmap::effects::ask_env;
676///
677/// fn get_full_env<Env>() -> impl Effect<Output = Env, Error = AnalysisError, Env = Env>
678/// where
679///     Env: AnalysisEnv + Clone + Send + Sync,
680/// {
681///     ask_env()
682/// }
683/// ```
684pub fn ask_env<Env>() -> stillwater::effect::reader::Ask<AnalysisError, Env>
685where
686    Env: Clone + Send + Sync + 'static,
687{
688    stillwater::ask::<AnalysisError, Env>()
689}
690
691// =============================================================================
692// Retry Pattern Helpers (Spec 205)
693// =============================================================================
694//
695// These helpers enable automatic retry of transient failures with configurable
696// backoff strategies.
697
698use crate::config::RetryConfig;
699use log::{error, info, warn};
700use std::time::Instant;
701
702/// Wrap an effect with retry logic using the configured policy.
703///
704/// This combinator automatically retries the effect when it fails with
705/// a retryable error, using the configured retry strategy and delays.
706///
707/// # Arguments
708///
709/// * `effect_factory` - A function that creates the effect to retry.
710///   Called each time a retry is needed.
711/// * `retry_config` - Configuration for retry behavior.
712///
713/// # Retryable Errors
714///
715/// Only errors where `error.is_retryable()` returns `true` will trigger
716/// a retry. Non-retryable errors (parse errors, validation errors, etc.)
717/// cause immediate failure.
718///
719/// # Example
720///
721/// ```rust,ignore
722/// use debtmap::effects::{with_retry, AnalysisEffect};
723/// use debtmap::config::RetryConfig;
724/// use debtmap::io::effects::read_file_effect;
725///
726/// fn read_file_resilient(path: PathBuf) -> AnalysisEffect<String> {
727///     let config = RetryConfig::default();
728///     with_retry(
729///         move || read_file_effect(path.clone()),
730///         config,
731///     )
732/// }
733/// ```
734///
735/// # Logging
736///
737/// Retry attempts are logged at WARN level for visibility:
738/// ```text
739/// WARN Retrying operation (attempt 2/3): I/O error: Resource busy
740/// ```
741pub fn with_retry<T, F>(effect_factory: F, retry_config: RetryConfig) -> AnalysisEffect<T>
742where
743    T: Send + 'static,
744    F: Fn() -> AnalysisEffect<T> + Send + Sync + 'static,
745{
746    from_async(move |env: &RealEnv| {
747        let env = env.clone();
748        let config = retry_config.clone();
749        let factory = SharedFn::new(effect_factory);
750
751        async move {
752            let start = Instant::now();
753            let mut attempt = 0u32;
754            let mut last_error: Option<AnalysisError> = None;
755
756            loop {
757                let effect = (factory.0)();
758                match effect.run(&env).await {
759                    Ok(value) => {
760                        if attempt > 0 {
761                            info!("Operation succeeded after {} retry attempt(s)", attempt);
762                        }
763                        return Ok(value);
764                    }
765                    Err(e) => {
766                        let elapsed = start.elapsed();
767
768                        // Check if error is retryable and we should try again
769                        if e.is_retryable() && config.should_retry(attempt, elapsed) {
770                            attempt += 1;
771                            warn!(
772                                "Retrying operation (attempt {}/{}): {}",
773                                attempt, config.max_retries, e
774                            );
775
776                            // Sleep before retry
777                            let delay = config.delay_for_attempt(attempt);
778                            tokio::time::sleep(delay).await;
779
780                            let _ = last_error.insert(e);
781                        } else {
782                            // Not retryable or exhausted retries
783                            if attempt > 0 {
784                                error!(
785                                    "Operation failed after {} retry attempt(s): {}",
786                                    attempt, e
787                                );
788                            }
789                            return Err(e);
790                        }
791                    }
792                }
793            }
794        }
795    })
796    .boxed()
797}
798
799/// Wrap an effect with retry logic, using the retry config from environment.
800///
801/// This is a convenience function that reads the retry configuration from
802/// the environment's config. If no retry config is set, uses defaults.
803///
804/// # Example
805///
806/// ```rust,ignore
807/// use debtmap::effects::with_retry_from_env;
808/// use debtmap::io::effects::read_file_effect;
809///
810/// fn read_file_resilient(path: PathBuf) -> AnalysisEffect<String> {
811///     with_retry_from_env(move || read_file_effect(path.clone()))
812/// }
813/// ```
814pub fn with_retry_from_env<T, F>(effect_factory: F) -> AnalysisEffect<T>
815where
816    T: Send + 'static,
817    F: Fn() -> AnalysisEffect<T> + Send + Sync + Clone + 'static,
818{
819    from_async(move |env: &RealEnv| {
820        let env = env.clone();
821        let factory = effect_factory.clone();
822
823        async move {
824            let config = env.config().retry.clone().unwrap_or_default();
825
826            // If retries are disabled, just run the effect directly
827            if !config.enabled {
828                return factory().run(&env).await;
829            }
830
831            let start = Instant::now();
832            let mut attempt = 0u32;
833
834            loop {
835                let effect = factory();
836                match effect.run(&env).await {
837                    Ok(value) => {
838                        if attempt > 0 {
839                            info!("Operation succeeded after {} retry attempt(s)", attempt);
840                        }
841                        return Ok(value);
842                    }
843                    Err(e) => {
844                        let elapsed = start.elapsed();
845
846                        if e.is_retryable() && config.should_retry(attempt, elapsed) {
847                            attempt += 1;
848                            warn!(
849                                "Retrying operation (attempt {}/{}): {}",
850                                attempt, config.max_retries, e
851                            );
852
853                            let delay = config.delay_for_attempt(attempt);
854                            tokio::time::sleep(delay).await;
855                        } else {
856                            if attempt > 0 {
857                                error!(
858                                    "Operation failed after {} retry attempt(s): {}",
859                                    attempt, e
860                                );
861                            }
862                            return Err(e);
863                        }
864                    }
865                }
866            }
867        }
868    })
869    .boxed()
870}
871
872/// Check if retries are enabled in the given config.
873///
874/// Returns `true` if the retry config is present and enabled.
875pub fn is_retry_enabled(config: &DebtmapConfig) -> bool {
876    config.retry.as_ref().map(|r| r.enabled).unwrap_or(true) // Default is enabled
877}
878
879/// Get the effective retry config from a DebtmapConfig.
880///
881/// Returns the configured retry settings or defaults if not set.
882pub fn get_retry_config(config: &DebtmapConfig) -> RetryConfig {
883    config.retry.clone().unwrap_or_default()
884}
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889
890    #[test]
891    fn test_validation_success() {
892        let v: AnalysisValidation<i32> = validation_success(42);
893        assert!(v.is_success());
894        match v {
895            Validation::Success(n) => assert_eq!(n, 42),
896            Validation::Failure(_) => panic!("Expected success"),
897        }
898    }
899
900    #[test]
901    fn test_validation_failure() {
902        let v: AnalysisValidation<i32> =
903            validation_failure(AnalysisError::validation("test error"));
904        assert!(v.is_failure());
905    }
906
907    #[test]
908    fn test_validation_failures() {
909        let errors = vec![
910            AnalysisError::validation("error 1"),
911            AnalysisError::validation("error 2"),
912        ];
913        let v: AnalysisValidation<i32> = validation_failures(errors);
914        assert!(v.is_failure());
915        match v {
916            Validation::Failure(nev) => {
917                let vec: Vec<_> = nev.into_iter().collect();
918                assert_eq!(vec.len(), 2);
919            }
920            _ => panic!("Expected failure"),
921        }
922    }
923
924    #[test]
925    fn test_run_validation_success() {
926        let v = validation_success(42);
927        let result = run_validation(v);
928        assert_eq!(result.unwrap(), 42);
929    }
930
931    #[test]
932    fn test_run_validation_failure() {
933        let v: AnalysisValidation<i32> =
934            validation_failure(AnalysisError::validation("test error"));
935        let result = run_validation(v);
936        assert!(result.is_err());
937        assert!(result.unwrap_err().to_string().contains("test error"));
938    }
939
940    #[test]
941    fn test_combine_validations_all_success() {
942        let validations = vec![
943            validation_success(1),
944            validation_success(2),
945            validation_success(3),
946        ];
947        let result = combine_validations(validations);
948        match result {
949            Validation::Success(values) => assert_eq!(values, vec![1, 2, 3]),
950            _ => panic!("Expected success"),
951        }
952    }
953
954    #[test]
955    fn test_combine_validations_accumulates_errors() {
956        let validations = vec![
957            validation_success(1),
958            validation_failure(AnalysisError::validation("error 1")),
959            validation_success(3),
960            validation_failure(AnalysisError::validation("error 2")),
961        ];
962        let result: AnalysisValidation<Vec<i32>> = combine_validations(validations);
963        match result {
964            Validation::Failure(errors) => {
965                let vec: Vec<_> = errors.into_iter().collect();
966                assert_eq!(vec.len(), 2);
967            }
968            _ => panic!("Expected failure with accumulated errors"),
969        }
970    }
971
972    #[test]
973    fn test_validation_map_success() {
974        let v = validation_success(21);
975        let v2 = validation_map(v, |n| n * 2);
976        match v2 {
977            Validation::Success(n) => assert_eq!(n, 42),
978            _ => panic!("Expected success"),
979        }
980    }
981
982    #[test]
983    fn test_validation_map_failure() {
984        let v: AnalysisValidation<i32> = validation_failure(AnalysisError::validation("error"));
985        let v2: AnalysisValidation<i32> = validation_map(v, |n| n * 2);
986        assert!(v2.is_failure());
987    }
988
989    #[test]
990    fn test_effect_pure() {
991        let effect = effect_pure(42);
992        let result = run_effect(effect, DebtmapConfig::default());
993        assert_eq!(result.unwrap(), 42);
994    }
995
996    #[test]
997    fn test_effect_fail() {
998        let effect: AnalysisEffect<i32> = effect_fail(AnalysisError::validation("test"));
999        let result = run_effect(effect, DebtmapConfig::default());
1000        assert!(result.is_err());
1001    }
1002
1003    #[test]
1004    fn test_run_effect() {
1005        let effect = effect_pure(42);
1006        let result = run_effect(effect, DebtmapConfig::default());
1007        assert_eq!(result.unwrap(), 42);
1008    }
1009
1010    // =========================================================================
1011    // Reader Pattern Tests (Spec 199)
1012    // =========================================================================
1013
1014    #[tokio::test]
1015    async fn test_asks_config_returns_config_value() {
1016        use crate::config::IgnoreConfig;
1017
1018        let config = DebtmapConfig {
1019            ignore: Some(IgnoreConfig {
1020                patterns: vec!["test/**".to_string()],
1021            }),
1022            ..Default::default()
1023        };
1024        let env = RealEnv::new(config);
1025
1026        // Create effect that queries config
1027        let effect = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
1028
1029        let patterns = effect.run(&env).await.unwrap();
1030        assert_eq!(patterns, vec!["test/**".to_string()]);
1031    }
1032
1033    #[tokio::test]
1034    async fn test_asks_config_with_default_config() {
1035        let env = RealEnv::default();
1036
1037        // Query ignore patterns from default config
1038        let effect = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
1039
1040        let patterns = effect.run(&env).await.unwrap();
1041        assert!(patterns.is_empty()); // Default config has no ignore patterns
1042    }
1043
1044    #[tokio::test]
1045    async fn test_asks_thresholds_with_thresholds() {
1046        use crate::config::ThresholdsConfig;
1047
1048        let config = DebtmapConfig {
1049            thresholds: Some(ThresholdsConfig {
1050                complexity: Some(15),
1051                max_file_length: Some(500),
1052                ..Default::default()
1053            }),
1054            ..Default::default()
1055        };
1056        let env = RealEnv::new(config);
1057
1058        let effect = asks_thresholds::<Option<u32>, RealEnv, _>(|thresholds| {
1059            thresholds.and_then(|t| t.complexity)
1060        });
1061
1062        let complexity = effect.run(&env).await.unwrap();
1063        assert_eq!(complexity, Some(15));
1064    }
1065
1066    #[tokio::test]
1067    async fn test_asks_thresholds_without_thresholds() {
1068        let env = RealEnv::default();
1069
1070        let effect = asks_thresholds::<Option<u32>, RealEnv, _>(|thresholds| {
1071            thresholds.and_then(|t| t.complexity)
1072        });
1073
1074        let complexity = effect.run(&env).await.unwrap();
1075        assert_eq!(complexity, None);
1076    }
1077
1078    #[tokio::test]
1079    async fn test_asks_scoring_with_weights() {
1080        let config = DebtmapConfig {
1081            scoring: Some(ScoringWeights {
1082                coverage: 0.6,
1083                complexity: 0.3,
1084                dependency: 0.1,
1085                ..Default::default()
1086            }),
1087            ..Default::default()
1088        };
1089        let env = RealEnv::new(config);
1090
1091        let effect =
1092            asks_scoring::<f64, RealEnv, _>(|scoring| scoring.map(|s| s.coverage).unwrap_or(0.5));
1093
1094        let coverage = effect.run(&env).await.unwrap();
1095        assert!((coverage - 0.6).abs() < 0.001);
1096    }
1097
1098    #[tokio::test]
1099    async fn test_asks_entropy_enabled() {
1100        let config = DebtmapConfig {
1101            entropy: Some(EntropyConfig {
1102                enabled: false,
1103                ..Default::default()
1104            }),
1105            ..Default::default()
1106        };
1107        let env = RealEnv::new(config);
1108
1109        let effect =
1110            asks_entropy::<bool, RealEnv, _>(|entropy| entropy.map(|e| e.enabled).unwrap_or(true));
1111
1112        let enabled = effect.run(&env).await.unwrap();
1113        assert!(!enabled);
1114    }
1115
1116    #[tokio::test]
1117    async fn test_local_with_config_modifies_config() {
1118        use crate::config::IgnoreConfig;
1119
1120        let original_config = DebtmapConfig {
1121            ignore: Some(IgnoreConfig {
1122                patterns: vec!["original/**".to_string()],
1123            }),
1124            ..Default::default()
1125        };
1126        let env = RealEnv::new(original_config);
1127
1128        // Inner effect that queries config
1129        let inner = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
1130
1131        // Wrap with local that modifies config
1132        let effect = local_with_config(
1133            |_config| DebtmapConfig {
1134                ignore: Some(IgnoreConfig {
1135                    patterns: vec!["modified/**".to_string()],
1136                }),
1137                ..Default::default()
1138            },
1139            inner,
1140        );
1141
1142        let patterns = effect.run(&env).await.unwrap();
1143        assert_eq!(patterns, vec!["modified/**".to_string()]);
1144    }
1145
1146    #[tokio::test]
1147    async fn test_local_with_config_restores_after() {
1148        use crate::config::IgnoreConfig;
1149
1150        let original_config = DebtmapConfig {
1151            ignore: Some(IgnoreConfig {
1152                patterns: vec!["original/**".to_string()],
1153            }),
1154            ..Default::default()
1155        };
1156        let env = RealEnv::new(original_config.clone());
1157
1158        // Run with modified config
1159        let inner = asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
1160        let modified_effect = local_with_config(
1161            |_| DebtmapConfig {
1162                ignore: Some(IgnoreConfig {
1163                    patterns: vec!["modified/**".to_string()],
1164                }),
1165                ..Default::default()
1166            },
1167            inner,
1168        );
1169        let _ = modified_effect.run(&env).await.unwrap();
1170
1171        // Original env should be unchanged (run a new query)
1172        let check_effect =
1173            asks_config::<Vec<String>, RealEnv, _>(|config| config.get_ignore_patterns());
1174        let patterns = check_effect.run(&env).await.unwrap();
1175        assert_eq!(patterns, vec!["original/**".to_string()]);
1176    }
1177
1178    #[tokio::test]
1179    async fn test_ask_env_returns_cloned_env() {
1180        let config = DebtmapConfig::default();
1181        let env = RealEnv::new(config);
1182
1183        let effect = ask_env::<RealEnv>();
1184
1185        let cloned_env = effect.run(&env).await.unwrap();
1186        // Both should have the same config
1187        assert_eq!(
1188            format!("{:?}", cloned_env.config()),
1189            format!("{:?}", env.config())
1190        );
1191    }
1192
1193    #[tokio::test]
1194    async fn test_reader_pattern_composition() {
1195        use stillwater::EffectExt;
1196
1197        let config = DebtmapConfig {
1198            scoring: Some(ScoringWeights {
1199                coverage: 0.6,
1200                complexity: 0.4,
1201                dependency: 0.0,
1202                ..Default::default()
1203            }),
1204            ..Default::default()
1205        };
1206        let env = RealEnv::new(config);
1207
1208        // Compose multiple Reader queries
1209        let coverage_effect =
1210            asks_scoring::<f64, RealEnv, _>(|scoring| scoring.map(|s| s.coverage).unwrap_or(0.5));
1211
1212        let complexity_effect = asks_scoring::<f64, RealEnv, _>(|scoring| {
1213            scoring.map(|s| s.complexity).unwrap_or(0.35)
1214        });
1215
1216        // Use and_then to compose effects
1217        let combined =
1218            coverage_effect.and_then(move |cov| complexity_effect.map(move |comp| cov + comp));
1219
1220        let sum = combined.run(&env).await.unwrap();
1221        assert!((sum - 1.0).abs() < 0.001); // 0.6 + 0.4 = 1.0
1222    }
1223
1224    // =========================================================================
1225    // Retry Pattern Tests (Spec 205)
1226    // =========================================================================
1227
1228    use std::sync::atomic::{AtomicUsize, Ordering};
1229
1230    #[tokio::test]
1231    async fn test_with_retry_succeeds_first_attempt() {
1232        let config = RetryConfig::default();
1233        let env = RealEnv::default();
1234
1235        let effect = with_retry(|| effect_pure(42), config);
1236        let result = effect.run(&env).await;
1237
1238        assert!(result.is_ok());
1239        assert_eq!(result.unwrap(), 42);
1240    }
1241
1242    #[tokio::test]
1243    async fn test_with_retry_succeeds_after_transient_failure() {
1244        let config = RetryConfig {
1245            max_retries: 3,
1246            base_delay_ms: 10, // Short delay for tests
1247            jitter_factor: 0.0,
1248            ..Default::default()
1249        };
1250        let env = RealEnv::default();
1251
1252        // Counter to track attempts
1253        let attempt_count = Arc::new(AtomicUsize::new(0));
1254        let attempt_clone = attempt_count.clone();
1255
1256        let effect = with_retry(
1257            move || {
1258                let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
1259                if count < 2 {
1260                    // Fail with retryable error for first 2 attempts
1261                    effect_fail(AnalysisError::io("Resource busy"))
1262                } else {
1263                    // Succeed on third attempt
1264                    effect_pure("success".to_string())
1265                }
1266            },
1267            config,
1268        );
1269
1270        let result = effect.run(&env).await;
1271
1272        assert!(result.is_ok());
1273        assert_eq!(result.unwrap(), "success");
1274        // Should have made 3 attempts (indices 0, 1, 2)
1275        assert_eq!(attempt_count.load(Ordering::SeqCst), 3);
1276    }
1277
1278    #[tokio::test]
1279    async fn test_with_retry_fails_on_permanent_error() {
1280        let config = RetryConfig {
1281            max_retries: 3,
1282            base_delay_ms: 10,
1283            jitter_factor: 0.0,
1284            ..Default::default()
1285        };
1286        let env = RealEnv::default();
1287
1288        let attempt_count = Arc::new(AtomicUsize::new(0));
1289        let attempt_clone = attempt_count.clone();
1290
1291        let effect: AnalysisEffect<String> = with_retry(
1292            move || {
1293                attempt_clone.fetch_add(1, Ordering::SeqCst);
1294                // Parse errors are not retryable
1295                effect_fail(AnalysisError::parse("Syntax error"))
1296            },
1297            config,
1298        );
1299
1300        let result = effect.run(&env).await;
1301
1302        assert!(result.is_err());
1303        // Should have only made 1 attempt (immediate failure, no retry)
1304        assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
1305    }
1306
1307    #[tokio::test]
1308    async fn test_with_retry_exhausts_retries() {
1309        let config = RetryConfig {
1310            max_retries: 2,
1311            base_delay_ms: 10,
1312            jitter_factor: 0.0,
1313            ..Default::default()
1314        };
1315        let env = RealEnv::default();
1316
1317        let attempt_count = Arc::new(AtomicUsize::new(0));
1318        let attempt_clone = attempt_count.clone();
1319
1320        let effect: AnalysisEffect<String> = with_retry(
1321            move || {
1322                attempt_clone.fetch_add(1, Ordering::SeqCst);
1323                // Always fail with retryable error
1324                effect_fail(AnalysisError::io("Resource busy"))
1325            },
1326            config,
1327        );
1328
1329        let result = effect.run(&env).await;
1330
1331        assert!(result.is_err());
1332        // Initial attempt + 2 retries = 3 total attempts
1333        assert_eq!(attempt_count.load(Ordering::SeqCst), 3);
1334    }
1335
1336    #[tokio::test]
1337    async fn test_with_retry_disabled() {
1338        let config = RetryConfig::disabled();
1339        let env = RealEnv::default();
1340
1341        let attempt_count = Arc::new(AtomicUsize::new(0));
1342        let attempt_clone = attempt_count.clone();
1343
1344        let effect: AnalysisEffect<String> = with_retry(
1345            move || {
1346                attempt_clone.fetch_add(1, Ordering::SeqCst);
1347                effect_fail(AnalysisError::io("Resource busy"))
1348            },
1349            config,
1350        );
1351
1352        let result = effect.run(&env).await;
1353
1354        assert!(result.is_err());
1355        // With retries disabled, should only try once
1356        assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
1357    }
1358
1359    #[tokio::test]
1360    async fn test_with_retry_from_env_uses_config() {
1361        let config = DebtmapConfig {
1362            retry: Some(RetryConfig {
1363                enabled: true,
1364                max_retries: 2,
1365                base_delay_ms: 10,
1366                jitter_factor: 0.0,
1367                ..Default::default()
1368            }),
1369            ..Default::default()
1370        };
1371        let env = RealEnv::new(config);
1372
1373        let attempt_count = Arc::new(AtomicUsize::new(0));
1374        let attempt_clone = attempt_count.clone();
1375
1376        let factory = move || {
1377            let count = attempt_clone.fetch_add(1, Ordering::SeqCst);
1378            if count < 1 {
1379                effect_fail(AnalysisError::io("Resource busy"))
1380            } else {
1381                effect_pure("success".to_string())
1382            }
1383        };
1384
1385        let effect = with_retry_from_env(factory);
1386        let result = effect.run(&env).await;
1387
1388        assert!(result.is_ok());
1389        assert_eq!(result.unwrap(), "success");
1390        // Should have made 2 attempts
1391        assert_eq!(attempt_count.load(Ordering::SeqCst), 2);
1392    }
1393
1394    #[tokio::test]
1395    async fn test_with_retry_from_env_disabled() {
1396        let config = DebtmapConfig {
1397            retry: Some(RetryConfig::disabled()),
1398            ..Default::default()
1399        };
1400        let env = RealEnv::new(config);
1401
1402        let attempt_count = Arc::new(AtomicUsize::new(0));
1403        let attempt_clone = attempt_count.clone();
1404
1405        let factory = move || {
1406            attempt_clone.fetch_add(1, Ordering::SeqCst);
1407            effect_fail::<String>(AnalysisError::io("Resource busy"))
1408        };
1409
1410        let effect = with_retry_from_env(factory);
1411        let result = effect.run(&env).await;
1412
1413        assert!(result.is_err());
1414        assert_eq!(attempt_count.load(Ordering::SeqCst), 1);
1415    }
1416
1417    #[test]
1418    fn test_is_retry_enabled_default() {
1419        let config = DebtmapConfig::default();
1420        assert!(is_retry_enabled(&config));
1421    }
1422
1423    #[test]
1424    fn test_is_retry_enabled_explicit_true() {
1425        let config = DebtmapConfig {
1426            retry: Some(RetryConfig {
1427                enabled: true,
1428                ..Default::default()
1429            }),
1430            ..Default::default()
1431        };
1432        assert!(is_retry_enabled(&config));
1433    }
1434
1435    #[test]
1436    fn test_is_retry_enabled_explicit_false() {
1437        let config = DebtmapConfig {
1438            retry: Some(RetryConfig::disabled()),
1439            ..Default::default()
1440        };
1441        assert!(!is_retry_enabled(&config));
1442    }
1443
1444    #[test]
1445    fn test_get_retry_config_default() {
1446        let config = DebtmapConfig::default();
1447        let retry_config = get_retry_config(&config);
1448
1449        assert!(retry_config.enabled);
1450        assert_eq!(retry_config.max_retries, 3);
1451    }
1452
1453    #[test]
1454    fn test_get_retry_config_custom() {
1455        let config = DebtmapConfig {
1456            retry: Some(RetryConfig {
1457                enabled: true,
1458                max_retries: 5,
1459                base_delay_ms: 200,
1460                ..Default::default()
1461            }),
1462            ..Default::default()
1463        };
1464        let retry_config = get_retry_config(&config);
1465
1466        assert!(retry_config.enabled);
1467        assert_eq!(retry_config.max_retries, 5);
1468        assert_eq!(retry_config.base_delay_ms, 200);
1469    }
1470}