Entropyk/_bmad-output/implementation-artifacts/coherence-audit-remediation-plan.md

16 KiB

Plan de Correction Post-Audit de Cohérence

Date: 2026-02-22
Auteur: Audit Architecture Rust & BMAD
Priorité: CRITIQUE
Statut: À implémenter


Résumé Exécutif

Cet audit a identifié 12 écarts de cohérence répartis en 3 catégories de priorité. Les problèmes les plus critiques concernent l'Epic 7 (Validation) où l'implémentation incomplète des méthodes energy_transfers() et port_enthalpies() empêche une validation thermodynamique correcte.

Impact métier: Sans ces corrections, un système thermodynamique contenant un détendeur, des jonctions ou des conditions aux limites NE sera PAS correctement validé, ce qui peut masquer des erreurs physiques graves.


Catalogue des User Stories de Correction

Epic 9: Correction de Cohérence (Nouvel Epic)

Story 9.1: Unification des Types Duplicats - CircuitId

Priorité: P1-CRITIQUE
Estimation: 2h
Dépendances: Aucune

Story:

En tant que développeur Rust,
Je veux un type CircuitId unique et cohérent,
Afin d'éviter les erreurs de compilation lors de l'utilisation conjointe des modules solver et components.

Problème actuel:

// crates/solver/src/system.rs:31
pub struct CircuitId(pub u8);  // Représentation numérique

// crates/components/src/state_machine.rs:332
pub struct CircuitId(String);  // Représentation textuelle

Solution proposée:

  1. Garder CircuitId(u8) dans crates/core/src/types.rs pour performance
  2. Ajouter impl From<&str> for CircuitId avec hash interne
  3. Supprimer CircuitId de state_machine.rs
  4. Ré-exporter depuis entropyk_core

Fichiers à modifier:

  • crates/core/src/types.rs (ajout)
  • crates/solver/src/system.rs (suppression, ré-import)
  • crates/components/src/state_machine.rs (suppression, ré-import)
  • crates/entropyk/src/lib.rs (mise à jour exports)

Critères d'acceptation:

  • Un seul CircuitId dans la codebase
  • Conversion From<&str> et From<u8> disponibles
  • cargo test --workspace passe
  • cargo clippy -- -D warnings passe

Story 9.2: Unification des Types Duplicats - FluidId

Priorité: P1-CRITIQUE
Estimation: 2h
Dépendances: Aucune

Story:

En tant que développeur Rust,
Je veux un type FluidId unique avec API cohérente,
Afin d'éviter la confusion entre fluids::FluidId et components::port::FluidId.

Problème actuel:

// crates/fluids/src/types.rs:35
pub struct FluidId(pub String);  // Champ public

// crates/components/src/port.rs:137
pub struct FluidId(String);  // Champ privé, méthode as_str()

Solution proposée:

  1. Garder FluidId dans crates/fluids/src/types.rs comme source unique
  2. Exposer as_str() et garder champ public pour compatibilité
  3. Supprimer FluidId de port.rs
  4. Ré-importer depuis entropyk_fluids

Fichiers à modifier:

  • crates/fluids/src/types.rs (ajout méthode as_str())
  • crates/components/src/port.rs (suppression, ré-import)
  • crates/components/src/lib.rs (mise à jour exports)

Critères d'acceptation:

  • Un seul FluidId dans la codebase
  • Méthode as_str() disponible
  • Champ 0 accessible pour compatibilité
  • cargo test --workspace passe

Story 9.3: Complétion Epic 7 - ExpansionValve Energy Methods

Priorité: P1-CRITIQUE
Estimation: 3h
Dépendances: Story 9.2

Story:

En tant que moteur de simulation thermodynamique,
Je veux que ExpansionValve implémente energy_transfers() et port_enthalpies(),
Afin que le bilan énergétique soit correctement validé pour les cycles frigorifiques.

Problème actuel:

  • ExpansionValve implémente seulement port_mass_flows()
  • Le détendeur est ignoré dans check_energy_balance()
  • Or, c'est un composant critique de tout cycle frigorifique

Solution proposée:

// Dans crates/components/src/expansion_valve.rs

impl Component for ExpansionValve<Connected> {
    // ... existing code ...
    
    fn port_enthalpies(
        &self,
        _state: &SystemState,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        // Retourne les enthalpies des ports (ordre: inlet, outlet)
        Ok(vec![
            self.port_inlet.enthalpy(),
            self.port_outlet.enthalpy(),
        ])
    }
    
    fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
        // Détendeur isenthalpique: Q=0, W=0
        // (Pas de transfert thermique, pas de travail)
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }
}

Fichiers à modifier:

  • crates/components/src/expansion_valve.rs

Critères d'acceptation:

  • energy_transfers() retourne (Power(0), Power(0))
  • port_enthalpies() retourne [h_in, h_out]
  • Test unitaire test_expansion_valve_energy_balance passe
  • check_energy_balance() ne skip plus ExpansionValve

Story 9.4: Complétion Epic 7 - FlowSource/FlowSink Energy Methods

Priorité: P1-CRITIQUE
Estimation: 3h
Dépendances: Story 9.2

Story:

En tant que moteur de simulation thermodynamique,
Je veux que FlowSource et FlowSink implémentent energy_transfers() et port_enthalpies(),
Afin que les conditions aux limites soient correctement prises en compte dans le bilan énergétique.

Problème actuel:

  • FlowSource et FlowSink implémentent seulement port_mass_flows()
  • Ces composants sont ignorés dans la validation

Solution proposée:

// Dans crates/components/src/flow_boundary.rs

impl Component for FlowSource {
    // ... existing code ...
    
    fn port_enthalpies(
        &self,
        _state: &SystemState,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        Ok(vec![self.port.enthalpy()])
    }
    
    fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
        // Source: pas de transfert actif, le fluide "apparaît" avec son enthalpie
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }
}

impl Component for FlowSink {
    // ... existing code ...
    
    fn port_enthalpies(
        &self,
        _state: &SystemState,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        Ok(vec![self.port.enthalpy()])
    }
    
    fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
        // Sink: pas de transfert actif
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }
}

Fichiers à modifier:

  • crates/components/src/flow_boundary.rs

Critères d'acceptation:

  • FlowSource et FlowSink implémentent les 3 méthodes
  • Tests unitaires associés passent
  • check_energy_balance() ne skip plus ces composants

Story 9.5: Complétion Epic 7 - FlowSplitter/FlowMerger Energy Methods

Priorité: P1-CRITIQUE
Estimation: 4h
Dépendances: Story 9.2

Story:

En tant que moteur de simulation thermodynamique,
Je veux que FlowSplitter et FlowMerger implémentent energy_transfers() et port_enthalpies(),
Afin que les jonctions soient correctement prises en compte dans le bilan énergétique.

Problème actuel:

  • FlowSplitter et FlowMerger implémentent seulement port_mass_flows()
  • Les jonctions sont ignorées dans la validation

Solution proposée:

// Dans crates/components/src/flow_junction.rs

impl Component for FlowSplitter {
    // ... existing code ...
    
    fn port_enthalpies(
        &self,
        _state: &SystemState,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        // Ordre: inlet, puis outlets
        let mut enthalpies = vec![self.port_inlet.enthalpy()];
        for outlet in &self.ports_outlet {
            enthalpies.push(outlet.enthalpy());
        }
        Ok(enthalpies)
    }
    
    fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
        // Jonction adiabatique: Q=0, W=0
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }
}

impl Component for FlowMerger {
    // ... existing code ...
    
    fn port_enthalpies(
        &self,
        _state: &SystemState,
    ) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
        // Ordre: inlets, puis outlet
        let mut enthalpies = Vec::new();
        for inlet in &self.ports_inlet {
            enthalpies.push(inlet.enthalpy());
        }
        enthalpies.push(self.port_outlet.enthalpy());
        Ok(enthalpies)
    }
    
    fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
        // Jonction adiabatique: Q=0, W=0
        Some((Power::from_watts(0.0), Power::from_watts(0.0)))
    }
}

Fichiers à modifier:

  • crates/components/src/flow_junction.rs

Critères d'acceptation:

  • FlowSplitter et FlowMerger implémentent les 3 méthodes
  • Tests unitaires associés passent
  • check_energy_balance() ne skip plus ces composants

Story 9.6: Amélioration Logging Validation Énergie

Priorité: P2-IMPORTANTE
Estimation: 1h
Dépendances: Stories 9.3, 9.4, 9.5

Story:

En tant que développeur debuggant une simulation,
Je veux un avertissement explicite quand des composants sont ignorés dans la validation énergétique,
Afin d'identifier rapidement les implémentations manquantes.

Problème actuel:

// crates/solver/src/system.rs:1873-1879
_ => {
    components_skipped += 1;
    tracing::debug!(  // ← Niveau DEBUG, pas WARNING
        node_index = node_idx.index(),
        "Component lacks full energy transfer or enthalpy data - skipping energy balance check"
    );
}

Solution proposée:

_ => {
    components_skipped += 1;
    tracing::warn!(
        node_index = node_idx.index(),
        component_type = std::any::type_name_of_val(component),
        "Component lacks energy_transfers() or port_enthalpies() - SKIPPED in energy balance validation"
    );
}

Fichiers à modifier:

  • crates/solver/src/system.rs

Critères d'acceptation:

  • Logging au niveau WARN (pas DEBUG)
  • Inclut le type du composant dans le message
  • Test que le warning est bien émis

Story 9.7: Refactoring Solver - Scinder solver.rs

Priorité: P3-AMÉLIORATION
Estimation: 4h
Dépendances: Aucune

Story:

En tant que développeur maintenant le code,
Je veux que les stratégies de solver soient dans des fichiers séparés,
Afin d'améliorer la maintenabilité du code.

Problème actuel:

  • solver.rs fait ~2800 lignes
  • Architecture.md spécifie strategies/newton_raphson.rs, strategies/sequential_substitution.rs, strategies/fallback.rs

Solution proposée:

crates/solver/src/
├── lib.rs
├── solver.rs          # Trait Solver, SolverStrategy enum
├── strategies/
│   ├── mod.rs
│   ├── newton_raphson.rs
│   ├── sequential_substitution.rs
│   └── fallback.rs
├── system.rs
├── jacobian.rs
└── ...

Fichiers à créer/modifier:

  • crates/solver/src/strategies/mod.rs (nouveau)
  • crates/solver/src/strategies/newton_raphson.rs (nouveau)
  • crates/solver/src/strategies/sequential_substitution.rs (nouveau)
  • crates/solver/src/strategies/fallback.rs (nouveau)
  • crates/solver/src/solver.rs (réduit)
  • crates/solver/src/lib.rs (mise à jour exports)

Critères d'acceptation:

  • Chaque fichier < 500 lignes
  • cargo test --workspace passe
  • API publique inchangée

Story 9.8: Création SystemState Struct Dédié

Priorité: P3-AMÉLIORATION
Estimation: 6h
Dépendances: Stories 9.1, 9.2

Story:

En tant que développeur Rust,
Je veux un struct SystemState dédié au lieu d'un type alias,
Afin d'avoir une validation du layout et une meilleure sémantique.

Problème actuel:

// crates/components/src/lib.rs:180
pub type SystemState = Vec<f64>;

Solution proposée:

// crates/core/src/state.rs (nouveau fichier)

/// État du système thermodynamique.
/// 
/// Layout: [P_edge0, h_edge0, P_edge1, h_edge1, ...]
/// - P: Pression en Pascals
/// - h: Enthalpie en J/kg
#[derive(Debug, Clone)]
pub struct SystemState {
    data: Vec<f64>,
    edge_count: usize,
}

impl SystemState {
    pub fn new(edge_count: usize) -> Self {
        Self {
            data: vec![0.0; edge_count * 2],
            edge_count,
        }
    }
    
    pub fn pressure(&self, edge_idx: usize) -> Option<Pressure> {
        self.data.get(edge_idx * 2).map(|&p| Pressure::from_pascals(p))
    }
    
    pub fn enthalpy(&self, edge_idx: usize) -> Option<Enthalpy> {
        self.data.get(edge_idx * 2 + 1).map(|&h| Enthalpy::from_joules_per_kg(h))
    }
    
    pub fn set_pressure(&mut self, edge_idx: usize, p: Pressure) {
        if let Some(slot) = self.data.get_mut(edge_idx * 2) {
            *slot = p.to_pascals();
        }
    }
    
    pub fn set_enthalpy(&mut self, edge_idx: usize, h: Enthalpy) {
        if let Some(slot) = self.data.get_mut(edge_idx * 2 + 1) {
            *slot = h.to_joules_per_kg();
        }
    }
    
    pub fn as_slice(&self) -> &[f64] {
        &self.data
    }
    
    pub fn as_mut_slice(&mut self) -> &mut [f64] {
        &mut self.data
    }
}

Critères d'acceptation:

  • Struct SystemState avec méthodes d'accès typées
  • Migration progressive (compatibilité avec AsRef<[f64]>)
  • Tests unitaires pour accès par edge

Plan d'Exécution Recommandé

Sprint 1: Corrections Critiques (Semaine 1)

Jour Story Durée
Lundi AM 9.1 CircuitId Unification 2h
Lundi PM 9.2 FluidId Unification 2h
Mardi AM 9.3 ExpansionValve Energy 3h
Mardi PM 9.4 FlowSource/FlowSink Energy 3h
Mercredi AM 9.5 FlowSplitter/FlowMerger Energy 4h
Mercredi PM 9.6 Logging Improvement 1h
Jeudi Tests d'intégration complets 4h
Vendredi Code review & documentation 4h

Sprint 2: Améliorations (Semaine 2)

Jour Story Durée
Lundi-Mardi 9.7 Solver Refactoring 4h
Mercredi-Vendredi 9.8 SystemState Struct 6h

Tests de Validation Finale

Après implémentation de toutes les stories, exécuter:

# 1. Tests unitaires complets
cargo test --workspace

# 2. Tests d'intégration thermodynamique
cargo test --test refrigeration_cycle_integration
cargo test --test mass_balance_integration

# 3. Validation clippy stricte
cargo clippy -- -D warnings

# 4. Benchmark performance
cargo bench

# 5. Test de simulation complète
cargo run --example simple_cycle

Métriques de Succès

Métrique Avant Après
Types duplicats 2 (CircuitId, FluidId) 0
Composants sans energy_transfers() 5 0
Composants sans port_enthalpies() 5 0
Lignes dans solver.rs ~2800 ~500
Couverture validation énergie Partielle Complète

Annexes

A. Liste Complète des Composants et Méthodes

Composant port_mass_flows() port_enthalpies() energy_transfers()
Compressor
ExpansionValve
Pipe
Pump
Fan
FlowSource
FlowSink
FlowSplitter
FlowMerger
HeatExchanger
Evaporator
Condenser
Economizer
EvaporatorCoil
CondenserCoil

B. Références Architecture


Document généré par audit automatique - 2026-02-22