ggen-core 26.7.2

Core graph-aware code generation engine
Documentation
use crate::codegen::IncrementalCache;
use crate::graph::Graph;
use crate::manifest::GgenManifest;
use crate::utils::error::Result;
use std::fs;
use std::path::Path;

pub struct WatchCacheIntegration;

impl WatchCacheIntegration {
    pub fn detect_affected_rules(
        manifest: &GgenManifest, base_path: &Path, cache: &IncrementalCache,
    ) -> Result<AffectedRulesAnalysis> {
        // Read current ontology content
        let ontology_path = base_path.join(&manifest.ontology.source);
        let ontology_content = fs::read_to_string(&ontology_path).unwrap_or_default();

        // Check what changed (use empty graph for inference state)
        let empty_graph =
            Graph::new().map_err(|e| crate::utils::error::Error::new(&e.to_string()))?;
        let invalidation = cache.check_invalidation(manifest, &ontology_content, &empty_graph);

        let mut affected_rules = vec![];
        let mut high_impact_rules = vec![];
        let mut unaffected_rules = vec![];

        for rule in &manifest.generation.rules {
            let should_rerun = invalidation.changed_rules.iter().any(|r| r == &rule.name);

            if should_rerun {
                affected_rules.push(rule.name.clone());

                if !manifest.ontology.imports.is_empty() {
                    high_impact_rules.push(rule.name.clone());
                }
            } else {
                unaffected_rules.push(rule.name.clone());
            }
        }

        Ok(AffectedRulesAnalysis {
            changes: ChangeFlags {
                dirty: DirtyArtifacts {
                    manifest_changed: invalidation.artifact.manifest_changed,
                    ontology_changed: invalidation.artifact.ontology_changed,
                    inference_state_changed: invalidation.artifact.inference_state_changed,
                },
                rerun_all: invalidation.should_rerun_all,
            },
            affected_rule_count: affected_rules.len(),
            unaffected_rule_count: unaffected_rules.len(),
            affected_rules,
            high_impact_rules,
            unaffected_rules,
        })
    }

    pub fn get_rule_execution_order(
        analysis: &AffectedRulesAnalysis, manifest: &GgenManifest,
    ) -> Vec<String> {
        if analysis.changes.rerun_all {
            return manifest
                .generation
                .rules
                .iter()
                .map(|r| r.name.clone())
                .collect();
        }

        let mut ordered = vec![];

        for rule in &manifest.generation.rules {
            if analysis.affected_rules.contains(&rule.name) {
                ordered.push(rule.name.clone());
            }
        }

        ordered
    }

    pub fn estimate_speedup(analysis: &AffectedRulesAnalysis, total_rules: usize) -> f64 {
        if total_rules == 0 {
            return 1.0;
        }

        let unaffected_ratio = analysis.unaffected_rule_count as f64 / total_rules as f64;
        unaffected_ratio.mul_add(4.0, 1.0)
    }
}

/// Which workspace artifacts are dirty (≤3 bools)
#[derive(Debug, Clone, Copy, Default)]
pub struct DirtyArtifacts {
    pub manifest_changed: bool,
    pub ontology_changed: bool,
    pub inference_state_changed: bool,
}

/// Flags indicating what changed in the workspace
#[derive(Debug, Clone, Copy, Default)]
pub struct ChangeFlags {
    /// Individual artifact dirty flags
    pub dirty: DirtyArtifacts,
    /// Re-run all rules (overrides individual flags)
    pub rerun_all: bool,
}

pub struct AffectedRulesAnalysis {
    pub changes: ChangeFlags,
    pub affected_rule_count: usize,
    pub unaffected_rule_count: usize,
    pub affected_rules: Vec<String>,
    pub high_impact_rules: Vec<String>,
    pub unaffected_rules: Vec<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_speedup_calculation_no_changes() {
        let analysis = AffectedRulesAnalysis {
            changes: ChangeFlags {
                dirty: DirtyArtifacts::default(),
                rerun_all: false,
            },
            affected_rule_count: 0,
            unaffected_rule_count: 10,
            affected_rules: vec![],
            high_impact_rules: vec![],
            unaffected_rules: (0..10).map(|i| format!("rule-{}", i)).collect(),
        };

        let speedup = WatchCacheIntegration::estimate_speedup(&analysis, 10);
        assert!(speedup > 1.0);
    }

    #[test]
    fn test_speedup_calculation_all_changed() {
        let analysis = AffectedRulesAnalysis {
            changes: ChangeFlags {
                dirty: DirtyArtifacts {
                    manifest_changed: true,
                    ..Default::default()
                },
                rerun_all: true,
            },
            affected_rule_count: 10,
            unaffected_rule_count: 0,
            affected_rules: (0..10).map(|i| format!("rule-{}", i)).collect(),
            high_impact_rules: vec![],
            unaffected_rules: vec![],
        };

        let speedup = WatchCacheIntegration::estimate_speedup(&analysis, 10);
        assert_eq!(speedup, 1.0);
    }

    #[test]
    fn test_rule_execution_order_partial() {
        use std::collections::BTreeMap;

        let mut manifest = GgenManifest {
            project: crate::manifest::ProjectConfig {
                name: "test".to_string(),
                version: "1.0.0".to_string(),
                description: None,
                ..Default::default()
            },
            ontology: crate::manifest::OntologyConfig {
                source: "ontology.ttl".into(),
                imports: vec![],
                base_iri: None,
                prefixes: BTreeMap::new(),
                ..Default::default()
            },
            inference: crate::manifest::InferenceConfig::default(),
            generation: crate::manifest::GenerationConfig {
                rules: vec![],
                output_dir: "output".into(),
                require_audit_trail: false,
                determinism_salt: None,
                max_sparql_timeout_ms: 5000,
                enable_llm: false,
                llm_model: None,
                llm_provider: None,
            },
            validation: crate::manifest::ValidationConfig::default(),
            packs: vec![],
            ..Default::default()
        };

        manifest.generation.rules = vec![
            crate::manifest::GenerationRule {
                name: "rule-1".to_string(),
                query: crate::manifest::QuerySource::Inline {
                    inline: "q1".to_string(),
                },
                template: crate::manifest::TemplateSource::Inline {
                    inline: "t1".to_string(),
                },
                output_file: "out1.rs".to_string(),
                skip_empty: false,
                mode: Default::default(),
                when: None,
            },
            crate::manifest::GenerationRule {
                name: "rule-2".to_string(),
                query: crate::manifest::QuerySource::Inline {
                    inline: "q2".to_string(),
                },
                template: crate::manifest::TemplateSource::Inline {
                    inline: "t2".to_string(),
                },
                output_file: "out2.rs".to_string(),
                skip_empty: false,
                mode: Default::default(),
                when: None,
            },
        ];

        let analysis = AffectedRulesAnalysis {
            changes: ChangeFlags {
                dirty: DirtyArtifacts {
                    ontology_changed: true,
                    ..Default::default()
                },
                rerun_all: false,
            },
            affected_rule_count: 1,
            unaffected_rule_count: 1,
            affected_rules: vec!["rule-1".to_string()],
            high_impact_rules: vec!["rule-1".to_string()],
            unaffected_rules: vec!["rule-2".to_string()],
        };

        let order = WatchCacheIntegration::get_rule_execution_order(&analysis, &manifest);
        assert_eq!(order, vec!["rule-1"]);
    }
}