Skip to main content

omena_transform_passes/runtime/
incremental.rs

1//! Incremental transform execution backed by `omena-incremental` graph inputs.
2//!
3//! This module derives stable dependency keys from source, dialect, pass plan,
4//! and transform context, then reuses a previous execution summary only when
5//! the incremental plan is clean.
6
7use omena_incremental::{
8    IncrementalGraphInputV0, IncrementalNodeInputV0, IncrementalRevisionV0,
9    OmenaIncrementalDatabaseV0,
10};
11use omena_parser::StyleDialect;
12use omena_transform_cst::TransformPassKind;
13
14use crate::{
15    TransformExecutionContextV0, TransformExecutionSummaryV0,
16    TransformIncrementalExecutionSummaryV0,
17    execute_transform_passes_on_source_with_dialect_and_context, plan_transform_passes,
18};
19
20pub fn execute_transform_passes_incremental_with_database(
21    source: &str,
22    dialect: StyleDialect,
23    requested: &[TransformPassKind],
24    context: &TransformExecutionContextV0,
25    incremental_database: &mut OmenaIncrementalDatabaseV0,
26    previous_execution: Option<&TransformExecutionSummaryV0>,
27    revision: IncrementalRevisionV0,
28) -> TransformIncrementalExecutionSummaryV0 {
29    let incremental_input =
30        transform_pass_incremental_graph_input(source, dialect, requested, context, revision);
31    let update = incremental_database.plan_and_upsert_graph_input(&incremental_input);
32    let reused_previous_execution =
33        update.incremental_plan.dirty_node_count == 0 && previous_execution.is_some();
34    let execution = match (reused_previous_execution, previous_execution) {
35        (true, Some(previous_execution)) => previous_execution.clone(),
36        _ => execute_transform_passes_on_source_with_dialect_and_context(
37            source, dialect, requested, context,
38        ),
39    };
40
41    TransformIncrementalExecutionSummaryV0 {
42        schema_version: "0",
43        product: "omena-transform-passes.incremental-execution",
44        incremental_engine: "omena-incremental",
45        query_model: "persistentSalsaDatabase+transformPassDependencyGraph",
46        reuse_policy: "reuse previous transform execution when the omena-incremental plan is clean",
47        reused_previous_execution,
48        incremental_plan: update.incremental_plan,
49        next_snapshot: update.next_snapshot,
50        execution,
51        ready_surfaces: vec![
52            "transformSalsaQueries",
53            "transformPassIncrementalGraph",
54            "cleanTransformExecutionReuse",
55        ],
56    }
57}
58
59pub fn transform_pass_incremental_graph_input(
60    source: &str,
61    dialect: StyleDialect,
62    requested: &[TransformPassKind],
63    context: &TransformExecutionContextV0,
64    revision: IncrementalRevisionV0,
65) -> IncrementalGraphInputV0 {
66    let pass_plan = plan_transform_passes(requested);
67    let dialect_label = transform_style_dialect_label(dialect);
68    let context_digest = transform_execution_context_digest(context);
69    let ordered_pass_ids = pass_plan.ordered_pass_ids.join("|");
70    let mut nodes = vec![
71        IncrementalNodeInputV0 {
72            id: "transform:source".to_string(),
73            digest: stable_transform_digest(&["source", dialect_label, source]),
74            dependency_ids: Vec::new(),
75        },
76        IncrementalNodeInputV0 {
77            id: "transform:context".to_string(),
78            digest: stable_transform_digest(&["context", context_digest.as_str()]),
79            dependency_ids: Vec::new(),
80        },
81        IncrementalNodeInputV0 {
82            id: "transform:plan".to_string(),
83            digest: stable_transform_digest(&["plan", ordered_pass_ids.as_str()]),
84            dependency_ids: Vec::new(),
85        },
86    ];
87
88    let mut previous_pass_node_id = None;
89    for pass_id in pass_plan.ordered_pass_ids {
90        let node_id = format!("transform:pass:{pass_id}");
91        let mut dependency_ids = vec![
92            "transform:source".to_string(),
93            "transform:context".to_string(),
94            "transform:plan".to_string(),
95        ];
96        if let Some(previous_pass_node_id) = previous_pass_node_id {
97            dependency_ids.push(previous_pass_node_id);
98        }
99
100        nodes.push(IncrementalNodeInputV0 {
101            id: node_id.clone(),
102            digest: stable_transform_digest(&["pass", pass_id]),
103            dependency_ids,
104        });
105        previous_pass_node_id = Some(node_id);
106    }
107
108    let mut execution_dependency_ids = vec![
109        "transform:source".to_string(),
110        "transform:context".to_string(),
111        "transform:plan".to_string(),
112    ];
113    if let Some(previous_pass_node_id) = previous_pass_node_id {
114        execution_dependency_ids.push(previous_pass_node_id);
115    }
116    nodes.push(IncrementalNodeInputV0 {
117        id: "transform:execution".to_string(),
118        digest: stable_transform_digest(&["execution", ordered_pass_ids.as_str()]),
119        dependency_ids: execution_dependency_ids,
120    });
121
122    IncrementalGraphInputV0 { revision, nodes }
123}
124
125fn transform_style_dialect_label(dialect: StyleDialect) -> &'static str {
126    match dialect {
127        StyleDialect::Css => "css",
128        StyleDialect::Scss => "scss",
129        StyleDialect::Sass => "sass",
130        StyleDialect::Less => "less",
131    }
132}
133
134fn transform_execution_context_digest(context: &TransformExecutionContextV0) -> String {
135    let serialized = match serde_json::to_string(context) {
136        Ok(serialized) => serialized,
137        Err(error) => format!("serialization-error:{error}"),
138    };
139    stable_transform_digest(&["transform-context", serialized.as_str()])
140}
141
142fn stable_transform_digest(parts: &[&str]) -> String {
143    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
144    for part in parts {
145        for byte in part.as_bytes() {
146            hash ^= u64::from(*byte);
147            hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
148        }
149        hash ^= 0xff;
150        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
151    }
152    format!("fnv1a64:{hash:016x}")
153}