Skip to main content

omena_incremental/
lib.rs

1//! Salsa-backed incremental computation substrate for omena-css.
2//!
3//! The crate owns graph snapshots, dirty-set planning, cancellation state, and
4//! fuzzable consistency cases. Downstream parser, semantic, and transform
5//! crates depend on these stable V0 payloads instead of reaching into Salsa
6//! internals directly.
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use omena_evidence_graph::{
11    EvidenceDemandEdgeV0, EvidenceGraphBuildErrorV0, EvidenceGraphV0, EvidenceNodeKeyV0,
12    EvidenceNodeSeedV0, FamilyStampV0, GuaranteeKindV0, SampledFixtureCorpusTokenV0,
13    build_evidence_graph_from_edges_v0,
14};
15use salsa::Setter;
16use serde::{Deserialize, Serialize};
17
18mod frame_invalidation;
19pub use frame_invalidation::*;
20
21#[cfg(test)]
22use std::cell::RefCell;
23#[cfg(test)]
24thread_local! {
25    static SALSA_NODE_VALUE_QUERY_RUNS_BY_ID: RefCell<BTreeMap<String, usize>> = const { RefCell::new(BTreeMap::new()) };
26}
27
28#[cfg(test)]
29fn record_salsa_node_value_query_run(id: &str) {
30    SALSA_NODE_VALUE_QUERY_RUNS_BY_ID.with(|runs| {
31        *runs.borrow_mut().entry(id.to_string()).or_default() += 1;
32    });
33}
34
35#[cfg(test)]
36fn reset_salsa_node_value_query_runs() {
37    SALSA_NODE_VALUE_QUERY_RUNS_BY_ID.with(|runs| runs.borrow_mut().clear());
38}
39
40#[cfg(test)]
41fn salsa_node_value_query_runs(id: &str) -> usize {
42    SALSA_NODE_VALUE_QUERY_RUNS_BY_ID
43        .with(|runs| runs.borrow().get(id).copied().unwrap_or_default())
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct OmenaIncrementalBoundarySummaryV0 {
49    pub schema_version: &'static str,
50    pub product: &'static str,
51    pub engine_name: &'static str,
52    pub invalidation_model: &'static str,
53    pub query_model: &'static str,
54    pub dependency_propagation_policy: &'static str,
55    pub maximum_dependency_propagation_iterations: &'static str,
56    pub node_identity: Vec<&'static str>,
57    pub dirty_reasons: Vec<&'static str>,
58    pub ready_surfaces: Vec<&'static str>,
59}
60
61pub const DEFAULT_INCREMENTAL_CANCELLATION_LIMIT: usize = 128;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct IncrementalRevisionV0 {
66    pub value: u64,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct OmenaWorkspaceSnapshotIdV0 {
72    pub value: u64,
73}
74
75impl OmenaWorkspaceSnapshotIdV0 {
76    pub fn from_revision(revision: IncrementalRevisionV0) -> Self {
77        Self {
78            value: revision.value,
79        }
80    }
81
82    pub fn revision(self) -> IncrementalRevisionV0 {
83        IncrementalRevisionV0 { value: self.value }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct IncrementalGraphInputV0 {
89    pub revision: IncrementalRevisionV0,
90    pub nodes: Vec<IncrementalNodeInputV0>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct IncrementalNodeInputV0 {
95    pub id: String,
96    pub digest: String,
97    pub dependency_ids: Vec<String>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct IncrementalSnapshotV0 {
103    pub schema_version: &'static str,
104    pub product: &'static str,
105    pub revision: IncrementalRevisionV0,
106    pub nodes: Vec<IncrementalSnapshotNodeV0>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
110#[serde(rename_all = "camelCase")]
111pub struct IncrementalSnapshotNodeV0 {
112    pub id: String,
113    pub digest: String,
114    pub dependency_ids: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct IncrementalComputationPlanV0 {
120    pub schema_version: &'static str,
121    pub product: &'static str,
122    pub revision: IncrementalRevisionV0,
123    pub node_count: usize,
124    pub dirty_node_count: usize,
125    pub changed_input_count: usize,
126    pub new_node_count: usize,
127    pub removed_node_count: usize,
128    pub dependency_dirty_count: usize,
129    pub alpha_equivalence_graph_hash: IncrementalAlphaEquivalenceHashV0,
130    pub shadow_delta_oracle: IncrementalShadowDeltaOracleV0,
131    pub invalidation_priority_plan: IncrementalInvalidationPriorityPlanV0,
132    pub nodes: Vec<IncrementalComputationNodeV0>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
136#[serde(rename_all = "camelCase")]
137pub struct IncrementalComputationNodeV0 {
138    pub id: String,
139    pub digest: String,
140    pub dependency_ids: Vec<String>,
141    pub dirty: bool,
142    pub reasons: Vec<&'static str>,
143    pub changed_at: IncrementalRevisionV0,
144    pub verified_at: IncrementalRevisionV0,
145    pub value_equal_to_previous: bool,
146    pub alpha_equivalence_hash: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct IncrementalAlphaEquivalenceHashV0 {
152    pub schema_version: &'static str,
153    pub product: &'static str,
154    pub feature_gate: &'static str,
155    pub claim_level: &'static str,
156    pub theorem_claimed: bool,
157    pub hash: String,
158    pub normalized_node_count: usize,
159    pub normalized_edge_count: usize,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct IncrementalShadowDeltaOracleV0 {
165    pub schema_version: &'static str,
166    pub product: &'static str,
167    pub feature_gate: &'static str,
168    pub claim_level: &'static str,
169    pub theorem_claimed: bool,
170    pub sampled_shadow_witness_ready: bool,
171    pub incremental_dirty_ids: Vec<String>,
172    pub from_scratch_dirty_ids: Vec<String>,
173    pub incremental_matches_from_scratch_delta: bool,
174    pub dbsp_zset_claim_ready: bool,
175    pub performance_benchmark_claim_ready: bool,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct IncrementalEditDistancePriorityInputV0 {
181    pub schema_version: &'static str,
182    pub product: &'static str,
183    pub feature_gate: &'static str,
184    pub claim_level: &'static str,
185    pub theorem_claimed: bool,
186    pub node_id: String,
187    pub edit_distance_total: usize,
188    pub cascade_margin_abs_distance: u64,
189    pub bridge_checked: bool,
190    pub bridge_calibration_stage: &'static str,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
194#[serde(rename_all = "camelCase")]
195pub struct IncrementalInvalidationPriorityPlanV0 {
196    pub schema_version: &'static str,
197    pub product: &'static str,
198    pub feature_gate: &'static str,
199    pub claim_level: &'static str,
200    pub theorem_claimed: bool,
201    pub public_safety_claim_ready: bool,
202    pub calibration_stage: &'static str,
203    pub weight_profile: &'static str,
204    pub metric_input_count: usize,
205    pub dirty_node_count: usize,
206    pub metric_consumed_count: usize,
207    pub prioritized_dirty_node_ids: Vec<String>,
208    pub entries: Vec<IncrementalInvalidationPriorityEntryV0>,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
212#[serde(rename_all = "camelCase")]
213pub struct IncrementalInvalidationPriorityEntryV0 {
214    pub node_id: String,
215    pub priority_rank: usize,
216    pub priority_score: u64,
217    pub priority_kind: &'static str,
218    pub metric_consumed: bool,
219    pub edit_distance_total: Option<usize>,
220    pub cascade_margin_abs_distance: Option<u64>,
221    pub bridge_checked: bool,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub struct IncrementalCancellationSnapshotV0 {
227    pub schema_version: &'static str,
228    pub product: &'static str,
229    pub cancelled_request_count: usize,
230    pub cancelled_request_ids: Vec<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
234#[serde(rename_all = "camelCase")]
235pub struct IncrementalDatabaseUpdateV0 {
236    pub schema_version: &'static str,
237    pub product: &'static str,
238    pub incremental_plan: IncrementalComputationPlanV0,
239    pub datalog_rule_evaluator: DatalogRuleEvaluatorV0,
240    pub next_snapshot: IncrementalSnapshotV0,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
244#[serde(rename_all = "camelCase")]
245pub struct IncrementalConsistencyFuzzCaseV0 {
246    pub seed: u64,
247    pub node_count: usize,
248    pub changed_node_index: Option<usize>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct IncrementalConsistencyFuzzResultV0 {
254    pub seed: u64,
255    pub node_count: usize,
256    pub changed_node_id: Option<String>,
257    pub dirty_node_count: usize,
258    pub expected_dirty_node_count: usize,
259    pub passed: bool,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
263#[serde(rename_all = "camelCase")]
264pub struct IncrementalFuzzSeedReportV0 {
265    pub schema_version: &'static str,
266    pub product: &'static str,
267    pub case_count: usize,
268    pub passed_count: usize,
269    pub failed_count: usize,
270    pub results: Vec<IncrementalConsistencyFuzzResultV0>,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
274#[serde(rename_all = "camelCase")]
275pub struct DatalogRuleEvaluatorRuleV0 {
276    pub name: &'static str,
277    pub head: &'static str,
278    pub body: Vec<&'static str>,
279    pub source: &'static str,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
283#[serde(rename_all = "camelCase")]
284/// V0 freeze-candidate typed contract over the incremental dirty-plan substrate.
285///
286/// This exposes Datalog-shaped rules and relations for auditability while the
287/// product path remains the Salsa-backed fixed-point planner. It is not an
288/// external Datalog host, FlowLog/Souffle/egglog binding, or Cargo 1.0 API.
289pub struct DatalogRuleEvaluatorV0 {
290    pub schema_version: &'static str,
291    pub product: &'static str,
292    pub evaluator_kind: &'static str,
293    pub substrate: &'static str,
294    pub external_host_ready: bool,
295    pub revision: IncrementalRevisionV0,
296    pub rule_count: usize,
297    pub relation_count: usize,
298    pub input_node_count: usize,
299    pub dirty_node_count: usize,
300    pub derived_node_count: usize,
301    pub iteration_limit: usize,
302    pub fixed_point_reached: bool,
303    pub relations: Vec<&'static str>,
304    pub rules: Vec<DatalogRuleEvaluatorRuleV0>,
305    pub incremental_plan: IncrementalComputationPlanV0,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
309#[serde(rename_all = "camelCase")]
310pub struct IncrementalLayerEvidenceV0 {
311    pub schema_version: &'static str,
312    pub product: &'static str,
313    pub claim_level: &'static str,
314    pub invalidation_layer: &'static str,
315    pub real_invalidation_evidence_ready: bool,
316    pub fuzz_evidence_ready: bool,
317    pub salsa_reuse_evidence_ready: bool,
318    pub datalog_contract_evidence_ready: bool,
319    pub value_equality_backdating_ready: bool,
320    pub alpha_equivalence_hash_ready: bool,
321    pub shadow_delta_oracle_ready: bool,
322    pub edit_distance_priority_ready: bool,
323    pub benchmark_surface_ready: bool,
324    pub performance_benchmark_claim_ready: bool,
325    pub external_datalog_host_ready: bool,
326    pub dbsp_zset_claim_ready: bool,
327    pub public_safety_claim_ready: bool,
328    pub benchmark_gate: &'static str,
329    pub benchmark_evidence_level: &'static str,
330    pub supported_claims: Vec<&'static str>,
331    pub deferred_claims: Vec<&'static str>,
332    pub boundary: OmenaIncrementalBoundarySummaryV0,
333    pub fuzz_report: IncrementalFuzzSeedReportV0,
334    pub sample_update: IncrementalDatabaseUpdateV0,
335}
336
337const INCREMENTAL_EVIDENCE_EDGE_KIND_V0: &str = "incremental-evidence";
338
339fn incremental_guarantee_kind(claim_level: &str) -> GuaranteeKindV0 {
340    GuaranteeKindV0::from_existing_label(claim_level)
341        .unwrap_or_else(GuaranteeKindV0::for_label_less_family)
342}
343
344fn incremental_evidence_node_key(
345    query_identity: impl Into<String>,
346    input_identity: impl Into<String>,
347) -> EvidenceNodeKeyV0 {
348    EvidenceNodeKeyV0::new(query_identity, input_identity)
349}
350
351fn incremental_evidence_edge(
352    from_query_identity: impl Into<String>,
353    to_node_key: EvidenceNodeKeyV0,
354) -> EvidenceDemandEdgeV0 {
355    EvidenceDemandEdgeV0::new(
356        from_query_identity,
357        to_node_key,
358        INCREMENTAL_EVIDENCE_EDGE_KIND_V0,
359    )
360}
361
362fn shadow_delta_family_stamp(oracle: &IncrementalShadowDeltaOracleV0) -> FamilyStampV0 {
363    match SampledFixtureCorpusTokenV0::from_matching_string_vectors(
364        &oracle.incremental_dirty_ids,
365        &oracle.from_scratch_dirty_ids,
366        oracle.incremental_matches_from_scratch_delta,
367    ) {
368        Some(token) => FamilyStampV0::sampled_fixture_corpus(&token),
369        None => FamilyStampV0::floor_assumption(),
370    }
371}
372
373impl IncrementalAlphaEquivalenceHashV0 {
374    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
375        incremental_evidence_node_key(self.product, format!("{}:{}", self.feature_gate, self.hash))
376    }
377
378    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
379        EvidenceNodeSeedV0::with_family(
380            self.evidence_node_key(),
381            vec![
382                self.product.to_string(),
383                self.feature_gate.to_string(),
384                self.claim_level.to_string(),
385            ],
386            incremental_guarantee_kind(self.claim_level),
387            FamilyStampV0::floor_assumption(),
388        )
389    }
390
391    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
392        incremental_evidence_edge(self.product, self.evidence_node_key())
393    }
394}
395
396impl IncrementalShadowDeltaOracleV0 {
397    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
398        incremental_evidence_node_key(
399            self.product,
400            format!(
401                "{}:incremental={}:fromScratch={}",
402                self.feature_gate,
403                self.incremental_dirty_ids.join(","),
404                self.from_scratch_dirty_ids.join(",")
405            ),
406        )
407    }
408
409    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
410        EvidenceNodeSeedV0::with_family(
411            self.evidence_node_key(),
412            vec![
413                self.product.to_string(),
414                self.feature_gate.to_string(),
415                self.claim_level.to_string(),
416            ],
417            incremental_guarantee_kind(self.claim_level),
418            shadow_delta_family_stamp(self),
419        )
420    }
421
422    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
423        incremental_evidence_edge(self.product, self.evidence_node_key())
424    }
425}
426
427impl IncrementalEditDistancePriorityInputV0 {
428    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
429        incremental_evidence_node_key(
430            self.product,
431            format!("{}:{}", self.feature_gate, self.node_id),
432        )
433    }
434
435    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
436        EvidenceNodeSeedV0::with_family(
437            self.evidence_node_key(),
438            vec![
439                self.product.to_string(),
440                self.feature_gate.to_string(),
441                self.claim_level.to_string(),
442                self.bridge_calibration_stage.to_string(),
443            ],
444            incremental_guarantee_kind(self.claim_level),
445            FamilyStampV0::floor_assumption(),
446        )
447    }
448
449    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
450        incremental_evidence_edge(self.product, self.evidence_node_key())
451    }
452}
453
454impl IncrementalInvalidationPriorityPlanV0 {
455    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
456        incremental_evidence_node_key(
457            self.product,
458            format!(
459                "{}:{}:dirty={}:metric={}",
460                self.feature_gate,
461                self.weight_profile,
462                self.dirty_node_count,
463                self.metric_consumed_count
464            ),
465        )
466    }
467
468    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
469        EvidenceNodeSeedV0::with_family(
470            self.evidence_node_key(),
471            vec![
472                self.product.to_string(),
473                self.feature_gate.to_string(),
474                self.claim_level.to_string(),
475                self.calibration_stage.to_string(),
476            ],
477            incremental_guarantee_kind(self.claim_level),
478            FamilyStampV0::floor_assumption(),
479        )
480    }
481
482    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
483        incremental_evidence_edge(self.product, self.evidence_node_key())
484    }
485}
486
487impl IncrementalComputationPlanV0 {
488    pub fn evidence_node_seeds(&self) -> Vec<EvidenceNodeSeedV0> {
489        vec![
490            self.alpha_equivalence_graph_hash.evidence_node_seed(),
491            self.shadow_delta_oracle.evidence_node_seed(),
492            self.invalidation_priority_plan.evidence_node_seed(),
493        ]
494    }
495
496    pub fn evidence_demand_edges(&self) -> Vec<EvidenceDemandEdgeV0> {
497        vec![
498            self.alpha_equivalence_graph_hash.evidence_demand_edge(),
499            self.shadow_delta_oracle.evidence_demand_edge(),
500            self.invalidation_priority_plan.evidence_demand_edge(),
501        ]
502    }
503
504    pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
505        build_evidence_graph_from_edges_v0(self.evidence_node_seeds(), self.evidence_demand_edges())
506    }
507}
508
509impl IncrementalLayerEvidenceV0 {
510    pub fn evidence_node_key(&self) -> EvidenceNodeKeyV0 {
511        incremental_evidence_node_key(self.product, self.invalidation_layer)
512    }
513
514    pub fn evidence_node_seed(&self) -> EvidenceNodeSeedV0 {
515        EvidenceNodeSeedV0::with_family(
516            self.evidence_node_key(),
517            vec![
518                self.product.to_string(),
519                self.claim_level.to_string(),
520                self.invalidation_layer.to_string(),
521                self.benchmark_evidence_level.to_string(),
522            ],
523            incremental_guarantee_kind(self.claim_level),
524            FamilyStampV0::floor_assumption(),
525        )
526    }
527
528    pub fn evidence_demand_edge(&self) -> EvidenceDemandEdgeV0 {
529        incremental_evidence_edge(self.product, self.evidence_node_key())
530    }
531
532    pub fn evidence_graph(&self) -> Result<EvidenceGraphV0, EvidenceGraphBuildErrorV0> {
533        let mut seeds = vec![self.evidence_node_seed()];
534        seeds.extend(self.sample_update.incremental_plan.evidence_node_seeds());
535        let mut edges = vec![self.evidence_demand_edge()];
536        edges.extend(self.sample_update.incremental_plan.evidence_demand_edges());
537        build_evidence_graph_from_edges_v0(seeds, edges)
538    }
539}
540
541#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct IncrementalCancellationRegistryV0 {
543    limit: usize,
544    cancelled_request_ids: BTreeSet<String>,
545}
546
547#[salsa::input(debug)]
548pub struct SalsaIncrementalNodeInputV0 {
549    #[returns(ref)]
550    id: String,
551    #[returns(ref)]
552    digest: String,
553    #[returns(ref)]
554    dependency_ids: Vec<String>,
555}
556
557#[salsa::input(debug)]
558pub struct SalsaIncrementalGraphInputV0 {
559    #[returns(ref)]
560    nodes: Vec<SalsaIncrementalNodeInputV0>,
561}
562
563#[salsa::input(debug)]
564pub struct SalsaIncrementalFileRevisionInputV0 {
565    file_id: u32,
566    revision: IncrementalRevisionV0,
567    #[returns(ref)]
568    syntax_node_id: String,
569}
570
571#[salsa::db]
572#[derive(Clone, Default)]
573pub struct OmenaSalsaDatabaseV0 {
574    storage: salsa::Storage<Self>,
575}
576
577#[salsa::db]
578impl salsa::Database for OmenaSalsaDatabaseV0 {}
579
580impl OmenaSalsaDatabaseV0 {
581    pub fn new() -> Self {
582        Self::default()
583    }
584
585    pub fn handle(&self) -> salsa::StorageHandle<Self> {
586        self.storage.clone().into_zalsa_handle()
587    }
588
589    pub fn from_handle(handle: salsa::StorageHandle<Self>) -> Self {
590        Self {
591            storage: handle.into_storage(),
592        }
593    }
594}
595
596#[derive(Default)]
597pub struct OmenaIncrementalDatabaseV0 {
598    db: OmenaSalsaDatabaseV0,
599    node_inputs_by_id: BTreeMap<String, SalsaIncrementalNodeInputV0>,
600    graph_input: Option<SalsaIncrementalGraphInputV0>,
601    current_snapshot: Option<IncrementalSnapshotV0>,
602}
603
604pub fn summarize_omena_incremental_boundary() -> OmenaIncrementalBoundarySummaryV0 {
605    OmenaIncrementalBoundarySummaryV0 {
606        schema_version: "0",
607        product: "omena-incremental.boundary",
608        engine_name: "omena-incremental",
609        invalidation_model: "stableNodeId+inputDigest+dependencyPropagation",
610        query_model: "salsaInput+trackedQueryFieldGranularReuse",
611        dependency_propagation_policy: "salsaDemandDirtySignatureReads",
612        maximum_dependency_propagation_iterations: "oracleOnlyNodeCount+1",
613        node_identity: vec!["id", "digest", "dependencyIds"],
614        dirty_reasons: vec![
615            "newNode",
616            "inputDigestChanged",
617            "dependencySetChanged",
618            "dependencyDirty",
619        ],
620        ready_surfaces: vec![
621            "incrementalGraphInput",
622            "incrementalSnapshot",
623            "incrementalComputationPlan",
624            "incrementalCancellationRegistry",
625            "datalogRuleEvaluatorV0",
626            "salsaPersistentDatabase",
627            "salsaTrackedNodeSnapshotQuery",
628            "salsaFieldGranularReuse",
629            "salsaPlanAndSnapshotUpdate",
630            "salsaDemandDependencyReads",
631            "valueEqualityBackdating",
632            "alphaEquivalenceHash",
633            "incrementalShadowDeltaOracle",
634            "editDistanceInvalidationPriority",
635        ],
636    }
637}
638
639pub fn summarize_datalog_rule_evaluator_v0(
640    input: &IncrementalGraphInputV0,
641    previous: Option<&IncrementalSnapshotV0>,
642) -> DatalogRuleEvaluatorV0 {
643    let mut database = OmenaIncrementalDatabaseV0::default();
644    if let Some(previous) = previous {
645        database.restore_snapshot(previous);
646    }
647    database
648        .plan_and_upsert_graph_input(input)
649        .datalog_rule_evaluator
650}
651
652fn summarize_datalog_rule_evaluator_for_plan_v0(
653    input: &IncrementalGraphInputV0,
654    incremental_plan: IncrementalComputationPlanV0,
655) -> DatalogRuleEvaluatorV0 {
656    let relations = vec![
657        "node(id,digest)",
658        "previousNode(id,digest)",
659        "dependsOn(nodeId,dependencyId)",
660        "changedInput(nodeId)",
661        "dirty(nodeId)",
662    ];
663    let rules = vec![
664        DatalogRuleEvaluatorRuleV0 {
665            name: "newNodeIsDirty",
666            head: "dirty(Node)",
667            body: vec!["node(Node,Digest)", "not previousNode(Node,_)"],
668            source: "omena-incremental.computation-plan",
669        },
670        DatalogRuleEvaluatorRuleV0 {
671            name: "changedDigestIsDirty",
672            head: "dirty(Node)",
673            body: vec![
674                "node(Node,Digest)",
675                "previousNode(Node,PreviousDigest)",
676                "Digest != PreviousDigest",
677            ],
678            source: "omena-incremental.computation-plan",
679        },
680        DatalogRuleEvaluatorRuleV0 {
681            name: "changedDependencySetIsDirty",
682            head: "dirty(Node)",
683            body: vec!["dependsOn(Node,_)", "previousDependencySetDiffers(Node)"],
684            source: "omena-incremental.computation-plan",
685        },
686        DatalogRuleEvaluatorRuleV0 {
687            name: "dependencyDirtyDemandRead",
688            head: "dirty(Node)",
689            body: vec!["dependsOn(Node,Dependency)", "dirty(Dependency)"],
690            source: "omena-incremental.salsa-demand-plan",
691        },
692    ];
693    let iteration_limit = dependency_propagation_iteration_limit(input.nodes.len());
694    let fixed_point_reached = dirty_set_is_dependency_closed(&incremental_plan);
695
696    DatalogRuleEvaluatorV0 {
697        schema_version: "0",
698        product: "omena-incremental.datalog-rule-evaluator",
699        evaluator_kind: "typedContractOverSalsaDemandPlan",
700        substrate: "omena-incremental.salsa-backed-computation-plan",
701        external_host_ready: false,
702        revision: input.revision,
703        rule_count: rules.len(),
704        relation_count: relations.len(),
705        input_node_count: input.nodes.len(),
706        dirty_node_count: incremental_plan.dirty_node_count,
707        derived_node_count: incremental_plan.dependency_dirty_count,
708        iteration_limit,
709        fixed_point_reached,
710        relations,
711        rules,
712        incremental_plan,
713    }
714}
715
716pub fn snapshot_from_graph_input(input: &IncrementalGraphInputV0) -> IncrementalSnapshotV0 {
717    IncrementalSnapshotV0 {
718        schema_version: "0",
719        product: "omena-incremental.snapshot",
720        revision: input.revision,
721        nodes: normalized_snapshot_nodes(input),
722    }
723}
724
725fn graph_input_from_snapshot(snapshot: &IncrementalSnapshotV0) -> IncrementalGraphInputV0 {
726    IncrementalGraphInputV0 {
727        revision: snapshot.revision,
728        nodes: snapshot
729            .nodes
730            .iter()
731            .map(|node| IncrementalNodeInputV0 {
732                id: node.id.clone(),
733                digest: node.digest.clone(),
734                dependency_ids: node.dependency_ids.clone(),
735            })
736            .collect(),
737    }
738}
739
740pub fn summarize_incremental_shadow_delta_oracle_v0(
741    input: &IncrementalGraphInputV0,
742    previous: Option<&IncrementalSnapshotV0>,
743    incremental_dirty_ids: BTreeSet<String>,
744) -> IncrementalShadowDeltaOracleV0 {
745    let from_scratch_dirty_ids = compute_from_scratch_delta_dirty_ids(input, previous);
746    let incremental_dirty_ids = incremental_dirty_ids.into_iter().collect::<Vec<_>>();
747    let from_scratch_dirty_ids = from_scratch_dirty_ids.into_iter().collect::<Vec<_>>();
748    let incremental_matches_from_scratch_delta = incremental_dirty_ids == from_scratch_dirty_ids;
749
750    IncrementalShadowDeltaOracleV0 {
751        schema_version: "0",
752        product: "omena-incremental.shadow-delta-oracle",
753        feature_gate: "incremental-shadow-delta-v0",
754        claim_level: "sampledFixtureWitnessNotEquivalenceProof",
755        theorem_claimed: false,
756        sampled_shadow_witness_ready: incremental_matches_from_scratch_delta,
757        incremental_dirty_ids,
758        from_scratch_dirty_ids,
759        incremental_matches_from_scratch_delta,
760        dbsp_zset_claim_ready: false,
761        performance_benchmark_claim_ready: false,
762    }
763}
764
765pub fn summarize_incremental_invalidation_priority_plan_v0(
766    nodes: &[IncrementalComputationNodeV0],
767    priority_inputs: &[IncrementalEditDistancePriorityInputV0],
768) -> IncrementalInvalidationPriorityPlanV0 {
769    let priority_inputs_by_node = priority_inputs
770        .iter()
771        .map(|input| (input.node_id.as_str(), input))
772        .collect::<BTreeMap<_, _>>();
773    let mut entries = nodes
774        .iter()
775        .filter(|node| node.dirty)
776        .map(|node| {
777            let priority_input = priority_inputs_by_node.get(node.id.as_str()).copied();
778            let edit_distance_total = priority_input.map(|input| input.edit_distance_total);
779            let cascade_margin_abs_distance =
780                priority_input.map(|input| input.cascade_margin_abs_distance);
781            let bridge_checked = priority_input.is_some_and(|input| input.bridge_checked);
782            let metric_consumed = priority_input.is_some();
783            let priority_score = invalidation_priority_score(priority_input);
784
785            IncrementalInvalidationPriorityEntryV0 {
786                node_id: node.id.clone(),
787                priority_rank: 0,
788                priority_score,
789                priority_kind: if metric_consumed {
790                    "editDistanceCascadeMarginWeighted"
791                } else {
792                    "dirtyNodeDefault"
793                },
794                metric_consumed,
795                edit_distance_total,
796                cascade_margin_abs_distance,
797                bridge_checked,
798            }
799        })
800        .collect::<Vec<_>>();
801    entries.sort_by(|left, right| {
802        right
803            .priority_score
804            .cmp(&left.priority_score)
805            .then_with(|| left.node_id.cmp(&right.node_id))
806    });
807    for (index, entry) in entries.iter_mut().enumerate() {
808        entry.priority_rank = index + 1;
809    }
810    let metric_consumed_count = entries.iter().filter(|entry| entry.metric_consumed).count();
811    let prioritized_dirty_node_ids = entries
812        .iter()
813        .map(|entry| entry.node_id.clone())
814        .collect::<Vec<_>>();
815
816    IncrementalInvalidationPriorityPlanV0 {
817        schema_version: "0",
818        product: "omena-incremental.invalidation-priority-plan",
819        feature_gate: "incremental-edit-distance-priority-v0",
820        claim_level: "fixtureWitnessSchedulerPriority",
821        theorem_claimed: false,
822        public_safety_claim_ready: false,
823        calibration_stage: "fixtureWitnessDistanceMarginWeightedV0",
824        weight_profile: "editDistance10+cascadeMargin3+bridgeChecked1",
825        metric_input_count: priority_inputs.len(),
826        dirty_node_count: entries.len(),
827        metric_consumed_count,
828        prioritized_dirty_node_ids,
829        entries,
830    }
831}
832
833fn invalidation_priority_score(
834    priority_input: Option<&IncrementalEditDistancePriorityInputV0>,
835) -> u64 {
836    const DIRTY_NODE_BASE_SCORE: u64 = 1_000;
837    let Some(priority_input) = priority_input else {
838        return DIRTY_NODE_BASE_SCORE;
839    };
840    DIRTY_NODE_BASE_SCORE
841        + (priority_input.edit_distance_total as u64).saturating_mul(10)
842        + priority_input.cascade_margin_abs_distance.saturating_mul(3)
843        + u64::from(priority_input.bridge_checked)
844}
845
846pub fn run_incremental_consistency_fuzz_case(
847    case: IncrementalConsistencyFuzzCaseV0,
848) -> IncrementalConsistencyFuzzResultV0 {
849    let node_count = case.node_count.clamp(1, 64);
850    let previous_input = generated_incremental_fuzz_graph(case.seed, node_count, None);
851    let previous_snapshot = snapshot_from_graph_input(&previous_input);
852    let changed_index = case
853        .changed_node_index
854        .map(|index| index.min(node_count.saturating_sub(1)));
855    let next_input = generated_incremental_fuzz_graph(case.seed, node_count, changed_index);
856    let mut database = OmenaIncrementalDatabaseV0::default();
857    database.restore_snapshot(&previous_snapshot);
858    let plan = database
859        .plan_and_upsert_graph_input(&next_input)
860        .incremental_plan;
861    let changed_node_id = changed_index.map(fuzz_node_id);
862    let expected_dirty_ids = changed_node_id
863        .as_ref()
864        .map(|changed_id| transitive_dependents(&next_input, changed_id))
865        .unwrap_or_default();
866    let actual_dirty_ids = plan
867        .nodes
868        .iter()
869        .filter(|node| node.dirty)
870        .map(|node| node.id.clone())
871        .collect::<BTreeSet<_>>();
872    let expected_dirty_node_count = expected_dirty_ids.len();
873    let passed = actual_dirty_ids == expected_dirty_ids
874        && plan.dirty_node_count == expected_dirty_node_count
875        && plan.changed_input_count == usize::from(changed_node_id.is_some());
876
877    IncrementalConsistencyFuzzResultV0 {
878        seed: case.seed,
879        node_count,
880        changed_node_id,
881        dirty_node_count: plan.dirty_node_count,
882        expected_dirty_node_count,
883        passed,
884    }
885}
886
887pub fn run_incremental_fuzz_seed_corpus() -> IncrementalFuzzSeedReportV0 {
888    let seeds = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233];
889    let results = seeds
890        .into_iter()
891        .enumerate()
892        .map(|(index, seed)| {
893            run_incremental_consistency_fuzz_case(IncrementalConsistencyFuzzCaseV0 {
894                seed,
895                node_count: index + 1,
896                changed_node_index: if index % 4 == 0 {
897                    None
898                } else {
899                    Some(index / 2)
900                },
901            })
902        })
903        .collect::<Vec<_>>();
904    let passed_count = results.iter().filter(|result| result.passed).count();
905    let case_count = results.len();
906
907    IncrementalFuzzSeedReportV0 {
908        schema_version: "0",
909        product: "omena-incremental.fuzz-seed-corpus",
910        case_count,
911        passed_count,
912        failed_count: case_count - passed_count,
913        results,
914    }
915}
916
917pub fn summarize_incremental_layer_evidence_v0() -> IncrementalLayerEvidenceV0 {
918    let boundary = summarize_omena_incremental_boundary();
919    let fuzz_report = run_incremental_fuzz_seed_corpus();
920    let mut database = OmenaIncrementalDatabaseV0::default();
921    let previous_input = IncrementalGraphInputV0 {
922        revision: IncrementalRevisionV0 { value: 1 },
923        nodes: vec![
924            IncrementalNodeInputV0 {
925                id: "source".to_string(),
926                digest: "source:v1".to_string(),
927                dependency_ids: Vec::new(),
928            },
929            IncrementalNodeInputV0 {
930                id: "style".to_string(),
931                digest: "style:v1".to_string(),
932                dependency_ids: vec!["source".to_string()],
933            },
934        ],
935    };
936    database.plan_and_upsert_graph_input(&previous_input);
937    let sample_priority_inputs = vec![IncrementalEditDistancePriorityInputV0 {
938        schema_version: "0",
939        product: "omena-incremental.edit-distance-priority-input",
940        feature_gate: "incremental-edit-distance-priority-v0",
941        claim_level: "fixtureWitnessMetricInput",
942        theorem_claimed: false,
943        node_id: "style".to_string(),
944        edit_distance_total: 3,
945        cascade_margin_abs_distance: 2,
946        bridge_checked: true,
947        bridge_calibration_stage: "fixtureWitnessOnlyUncalibrated",
948    }];
949    let sample_update = database.plan_and_upsert_graph_input_with_priority_inputs(
950        &IncrementalGraphInputV0 {
951            revision: IncrementalRevisionV0 { value: 2 },
952            nodes: vec![
953                IncrementalNodeInputV0 {
954                    id: "source".to_string(),
955                    digest: "source:v2".to_string(),
956                    dependency_ids: Vec::new(),
957                },
958                IncrementalNodeInputV0 {
959                    id: "style".to_string(),
960                    digest: "style:v1".to_string(),
961                    dependency_ids: vec!["source".to_string()],
962                },
963            ],
964        },
965        &sample_priority_inputs,
966    );
967
968    IncrementalLayerEvidenceV0 {
969        schema_version: "0",
970        product: "omena-incremental.layer-evidence",
971        claim_level: "m6IncrementalLayerEvidenceOnly",
972        invalidation_layer: "stableNodeIdDigestDependencyGraph",
973        real_invalidation_evidence_ready: sample_update.incremental_plan.changed_input_count == 1
974            && sample_update.incremental_plan.dependency_dirty_count == 1
975            && sample_update.incremental_plan.dirty_node_count == 2,
976        fuzz_evidence_ready: fuzz_report.failed_count == 0,
977        salsa_reuse_evidence_ready: boundary
978            .ready_surfaces
979            .contains(&"salsaTrackedNodeSnapshotQuery"),
980        datalog_contract_evidence_ready: sample_update.datalog_rule_evaluator.fixed_point_reached
981            && !sample_update.datalog_rule_evaluator.external_host_ready,
982        value_equality_backdating_ready: sample_update.incremental_plan.nodes.iter().any(|node| {
983            node.id == "style"
984                && node.value_equal_to_previous
985                && node.changed_at.value == 1
986                && node.verified_at.value == 2
987        }),
988        alpha_equivalence_hash_ready: sample_update
989            .incremental_plan
990            .alpha_equivalence_graph_hash
991            .feature_gate
992            == "incremental-alpha-equivalence-hash-v0"
993            && !sample_update
994                .incremental_plan
995                .alpha_equivalence_graph_hash
996                .theorem_claimed,
997        shadow_delta_oracle_ready: sample_update
998            .incremental_plan
999            .shadow_delta_oracle
1000            .incremental_matches_from_scratch_delta
1001            && !sample_update
1002                .incremental_plan
1003                .shadow_delta_oracle
1004                .theorem_claimed,
1005        edit_distance_priority_ready: sample_update
1006            .incremental_plan
1007            .invalidation_priority_plan
1008            .entries
1009            .iter()
1010            .any(|entry| {
1011                entry.node_id == "style"
1012                    && entry.metric_consumed
1013                    && entry.priority_kind == "editDistanceCascadeMarginWeighted"
1014            }),
1015        benchmark_surface_ready: true,
1016        performance_benchmark_claim_ready: false,
1017        external_datalog_host_ready: false,
1018        dbsp_zset_claim_ready: false,
1019        public_safety_claim_ready: false,
1020        benchmark_gate: "rust/z5-performance-baseline-readiness",
1021        benchmark_evidence_level: "configuredCriterionSurfaceNoTimingClaim",
1022        supported_claims: vec![
1023            "stable node id plus digest invalidation",
1024            "dependency dirty-set fixed point",
1025            "Salsa-backed tracked node snapshot reuse",
1026            "fuzzed dirty-set invariant corpus",
1027            "Datalog-shaped audit contract over the incremental plan",
1028            "value-equality backdating with changed_at/verified_at split",
1029            "alpha-equivalence-aware fixture hash",
1030            "sampled incremental-vs-from-scratch shadow delta oracle",
1031            "edit-distance weighted invalidation priority",
1032        ],
1033        deferred_claims: vec![
1034            "DBSP runtime",
1035            "Z-set differential dataflow semantics",
1036            "external Datalog host execution",
1037            "performance superiority from local timing data",
1038            "public safety claim",
1039        ],
1040        boundary,
1041        fuzz_report,
1042        sample_update,
1043    }
1044}
1045
1046#[salsa::tracked(returns(clone))]
1047pub fn summarize_salsa_incremental_node_snapshot(
1048    db: &dyn salsa::Database,
1049    node: SalsaIncrementalNodeInputV0,
1050) -> IncrementalSnapshotNodeV0 {
1051    IncrementalSnapshotNodeV0 {
1052        id: node.id(db).clone(),
1053        digest: node.digest(db).clone(),
1054        dependency_ids: normalized_ids(node.dependency_ids(db)),
1055    }
1056}
1057
1058#[salsa::tracked(returns(clone))]
1059pub fn read_salsa_incremental_node_digest(
1060    db: &dyn salsa::Database,
1061    node: SalsaIncrementalNodeInputV0,
1062) -> String {
1063    #[cfg(test)]
1064    omena_testkit::current_instrumentation_session_v0().record_salsa_digest_query_run();
1065
1066    node.digest(db).clone()
1067}
1068
1069#[salsa::tracked(returns(clone))]
1070pub fn read_salsa_incremental_node_dependency_ids(
1071    db: &dyn salsa::Database,
1072    node: SalsaIncrementalNodeInputV0,
1073) -> Vec<String> {
1074    #[cfg(test)]
1075    omena_testkit::current_instrumentation_session_v0().record_salsa_dependency_query_run();
1076
1077    normalized_ids(node.dependency_ids(db))
1078}
1079
1080#[salsa::tracked(returns(clone))]
1081fn read_salsa_incremental_node_dependency_edges(
1082    db: &dyn salsa::Database,
1083    node: SalsaIncrementalNodeInputV0,
1084) -> Vec<String> {
1085    normalized_ids(node.dependency_ids(db))
1086}
1087
1088#[cfg(test)]
1089#[salsa::tracked(returns(clone))]
1090fn read_salsa_transitive_leaf(
1091    db: &dyn salsa::Database,
1092    node: SalsaIncrementalNodeInputV0,
1093) -> String {
1094    omena_testkit::current_instrumentation_session_v0().record_salsa_transitive_leaf_query_run();
1095    node.digest(db).clone()
1096}
1097
1098#[cfg(test)]
1099#[salsa::tracked(returns(clone))]
1100fn read_salsa_transitive_a(db: &dyn salsa::Database, a: SalsaIncrementalNodeInputV0) -> String {
1101    omena_testkit::current_instrumentation_session_v0().record_salsa_transitive_a_query_run();
1102    format!("a={}", read_salsa_transitive_leaf(db, a))
1103}
1104
1105#[cfg(test)]
1106#[salsa::tracked(returns(clone))]
1107fn read_salsa_transitive_b(
1108    db: &dyn salsa::Database,
1109    a: SalsaIncrementalNodeInputV0,
1110    b: SalsaIncrementalNodeInputV0,
1111) -> String {
1112    omena_testkit::current_instrumentation_session_v0().record_salsa_transitive_b_query_run();
1113    format!(
1114        "{}|b={}",
1115        read_salsa_transitive_a(db, a),
1116        read_salsa_transitive_leaf(db, b)
1117    )
1118}
1119
1120#[cfg(test)]
1121#[salsa::tracked(returns(clone))]
1122fn read_salsa_transitive_c(
1123    db: &dyn salsa::Database,
1124    a: SalsaIncrementalNodeInputV0,
1125    b: SalsaIncrementalNodeInputV0,
1126    c: SalsaIncrementalNodeInputV0,
1127) -> String {
1128    omena_testkit::current_instrumentation_session_v0().record_salsa_transitive_c_query_run();
1129    format!(
1130        "{}|c={}",
1131        read_salsa_transitive_b(db, a, b),
1132        read_salsa_transitive_leaf(db, c)
1133    )
1134}
1135
1136#[cfg(test)]
1137#[salsa::tracked(returns(clone))]
1138fn read_salsa_transitive_unrelated(
1139    db: &dyn salsa::Database,
1140    node: SalsaIncrementalNodeInputV0,
1141) -> String {
1142    omena_testkit::current_instrumentation_session_v0()
1143        .record_salsa_transitive_unrelated_query_run();
1144    format!("u={}", read_salsa_transitive_leaf(db, node))
1145}
1146
1147#[salsa::tracked(returns(clone))]
1148pub fn read_salsa_file_revision_syntax_key(
1149    db: &dyn salsa::Database,
1150    input: SalsaIncrementalFileRevisionInputV0,
1151) -> String {
1152    let revision = input.revision(db);
1153    format!(
1154        "file={};revision={};syntax={}",
1155        input.file_id(db),
1156        revision.value,
1157        input.syntax_node_id(db)
1158    )
1159}
1160
1161pub fn read_salsa_incremental_node_value(
1162    db: &dyn salsa::Database,
1163    graph: SalsaIncrementalGraphInputV0,
1164    node: SalsaIncrementalNodeInputV0,
1165) -> String {
1166    read_salsa_incremental_node_value_with_path(db, graph, node, String::new())
1167}
1168
1169pub fn read_salsa_incremental_node_dirty_signature(
1170    db: &dyn salsa::Database,
1171    graph: SalsaIncrementalGraphInputV0,
1172    node: SalsaIncrementalNodeInputV0,
1173) -> String {
1174    read_salsa_incremental_node_dirty_signature_with_path(db, graph, node, String::new())
1175}
1176
1177#[salsa::tracked(returns(clone))]
1178fn read_salsa_incremental_node_value_with_path(
1179    db: &dyn salsa::Database,
1180    graph: SalsaIncrementalGraphInputV0,
1181    node: SalsaIncrementalNodeInputV0,
1182    path_key: String,
1183) -> String {
1184    let id = node.id(db).clone();
1185    #[cfg(test)]
1186    record_salsa_node_value_query_run(id.as_str());
1187
1188    if path_contains_id(path_key.as_str(), id.as_str()) {
1189        return format!("{id}=<cycle>");
1190    }
1191
1192    let digest = node.digest(db).clone();
1193    let next_path = append_path_id(path_key.as_str(), id.as_str());
1194    let dependency_values = read_salsa_incremental_node_dependency_edges(db, node)
1195        .into_iter()
1196        .map(|dependency_id| {
1197            if path_contains_id(next_path.as_str(), dependency_id.as_str()) {
1198                return format!("{dependency_id}=<cycle>");
1199            }
1200            find_salsa_incremental_node_by_id(db, graph, dependency_id.as_str())
1201                .map(|dependency| {
1202                    read_salsa_incremental_node_value_with_path(
1203                        db,
1204                        graph,
1205                        dependency,
1206                        next_path.clone(),
1207                    )
1208                })
1209                .unwrap_or_else(|| format!("{dependency_id}=<missing>"))
1210        })
1211        .collect::<Vec<_>>();
1212
1213    format!("{id}={digest};deps=[{}]", dependency_values.join(","))
1214}
1215
1216#[salsa::tracked(returns(clone))]
1217fn read_salsa_incremental_node_dirty_signature_with_path(
1218    db: &dyn salsa::Database,
1219    graph: SalsaIncrementalGraphInputV0,
1220    node: SalsaIncrementalNodeInputV0,
1221    path_key: String,
1222) -> String {
1223    let id = node.id(db).clone();
1224    if path_contains_id(path_key.as_str(), id.as_str()) {
1225        return stable_hash_hex(format!("cycle:{id}").as_bytes());
1226    }
1227
1228    let digest = node.digest(db).clone();
1229    let next_path = append_path_id(path_key.as_str(), id.as_str());
1230    let dependency_signatures = read_salsa_incremental_node_dependency_edges(db, node)
1231        .into_iter()
1232        .map(|dependency_id| {
1233            if path_contains_id(next_path.as_str(), dependency_id.as_str()) {
1234                return stable_hash_hex(format!("cycle:{dependency_id}").as_bytes());
1235            }
1236            find_salsa_incremental_node_by_id(db, graph, dependency_id.as_str())
1237                .map(|dependency| {
1238                    read_salsa_incremental_node_dirty_signature_with_path(
1239                        db,
1240                        graph,
1241                        dependency,
1242                        next_path.clone(),
1243                    )
1244                })
1245                .unwrap_or_else(|| stable_hash_hex(format!("missing:{dependency_id}").as_bytes()))
1246        })
1247        .collect::<Vec<_>>();
1248    let signature = format!("digest={digest};deps=[{}]", dependency_signatures.join(","));
1249    stable_hash_hex(signature.as_bytes())
1250}
1251
1252fn find_salsa_incremental_node_by_id(
1253    db: &dyn salsa::Database,
1254    graph: SalsaIncrementalGraphInputV0,
1255    id: &str,
1256) -> Option<SalsaIncrementalNodeInputV0> {
1257    graph
1258        .nodes(db)
1259        .iter()
1260        .find(|node| node.id(db).as_str() == id)
1261        .copied()
1262}
1263
1264fn path_contains_id(path_key: &str, id: &str) -> bool {
1265    path_key.split('\n').any(|entry| entry == id)
1266}
1267
1268fn append_path_id(path_key: &str, id: &str) -> String {
1269    if path_key.is_empty() {
1270        id.to_string()
1271    } else {
1272        format!("{path_key}\n{id}")
1273    }
1274}
1275
1276fn normalized_snapshot_nodes(input: &IncrementalGraphInputV0) -> Vec<IncrementalSnapshotNodeV0> {
1277    let mut nodes = input
1278        .nodes
1279        .iter()
1280        .map(|node| IncrementalSnapshotNodeV0 {
1281            id: node.id.clone(),
1282            digest: node.digest.clone(),
1283            dependency_ids: normalized_ids(&node.dependency_ids),
1284        })
1285        .collect::<Vec<_>>();
1286    nodes.sort_by(|left, right| left.id.cmp(&right.id));
1287    nodes
1288}
1289
1290fn normalized_existing_snapshot_nodes(
1291    nodes: &[IncrementalSnapshotNodeV0],
1292) -> Vec<IncrementalSnapshotNodeV0> {
1293    let mut nodes = nodes
1294        .iter()
1295        .map(|node| IncrementalSnapshotNodeV0 {
1296            id: node.id.clone(),
1297            digest: node.digest.clone(),
1298            dependency_ids: normalized_ids(&node.dependency_ids),
1299        })
1300        .collect::<Vec<_>>();
1301    nodes.sort_by(|left, right| left.id.cmp(&right.id));
1302    nodes
1303}
1304
1305fn normalized_ids(ids: &[String]) -> Vec<String> {
1306    ids.iter()
1307        .cloned()
1308        .collect::<BTreeSet<_>>()
1309        .into_iter()
1310        .collect()
1311}
1312
1313fn summarize_alpha_equivalence_hash(
1314    input: &IncrementalGraphInputV0,
1315) -> IncrementalAlphaEquivalenceHashV0 {
1316    let node_hashes = alpha_equivalence_hashes_by_node(input);
1317    let normalized_node_count = node_hashes.len();
1318    let normalized_edge_count = input
1319        .nodes
1320        .iter()
1321        .map(|node| normalized_ids(&node.dependency_ids).len())
1322        .sum();
1323    let mut labels = node_hashes.into_values().collect::<Vec<_>>();
1324    labels.sort();
1325    let labels = labels.join("|");
1326    let hash = stable_hash_hex(
1327        format!("nodes={normalized_node_count};edges={normalized_edge_count};labels={labels}")
1328            .as_bytes(),
1329    );
1330
1331    IncrementalAlphaEquivalenceHashV0 {
1332        schema_version: "0",
1333        product: "omena-incremental.alpha-equivalence-hash",
1334        feature_gate: "incremental-alpha-equivalence-hash-v0",
1335        claim_level: "fixtureWitnessAlphaRenamingStableHash",
1336        theorem_claimed: false,
1337        hash,
1338        normalized_node_count,
1339        normalized_edge_count,
1340    }
1341}
1342
1343fn alpha_equivalence_hashes_by_node(input: &IncrementalGraphInputV0) -> BTreeMap<String, String> {
1344    let nodes = normalized_snapshot_nodes(input);
1345    alpha_equivalence_hashes_for_snapshot_nodes(&nodes)
1346}
1347
1348fn alpha_equivalence_hashes_by_snapshot_nodes(
1349    nodes: &[IncrementalSnapshotNodeV0],
1350) -> BTreeMap<String, String> {
1351    let nodes = normalized_existing_snapshot_nodes(nodes);
1352    alpha_equivalence_hashes_for_snapshot_nodes(&nodes)
1353}
1354
1355fn alpha_equivalence_hashes_for_snapshot_nodes(
1356    nodes: &[IncrementalSnapshotNodeV0],
1357) -> BTreeMap<String, String> {
1358    let internal_ids = nodes
1359        .iter()
1360        .map(|node| node.id.as_str())
1361        .collect::<BTreeSet<_>>();
1362    let mut labels = nodes
1363        .iter()
1364        .map(|node| {
1365            (
1366                node.id.clone(),
1367                stable_hash_hex(format!("digest={}", node.digest).as_bytes()),
1368            )
1369        })
1370        .collect::<BTreeMap<_, _>>();
1371
1372    for _ in 0..=nodes.len() {
1373        let next = nodes
1374            .iter()
1375            .map(|node| {
1376                let mut dependency_labels = node
1377                    .dependency_ids
1378                    .iter()
1379                    .map(|dependency_id| {
1380                        if internal_ids.contains(dependency_id.as_str()) {
1381                            labels
1382                                .get(dependency_id)
1383                                .cloned()
1384                                .unwrap_or_else(|| stable_hash_hex(dependency_id.as_bytes()))
1385                        } else {
1386                            format!("external:{dependency_id}")
1387                        }
1388                    })
1389                    .collect::<Vec<_>>();
1390                dependency_labels.sort();
1391                let signature = format!(
1392                    "digest={};deps={}",
1393                    node.digest,
1394                    dependency_labels.join(",")
1395                );
1396                (node.id.clone(), stable_hash_hex(signature.as_bytes()))
1397            })
1398            .collect::<BTreeMap<_, _>>();
1399        if next == labels {
1400            break;
1401        }
1402        labels = next;
1403    }
1404
1405    labels
1406}
1407
1408fn unique_previous_nodes_by_alpha_hash<'a>(
1409    nodes: &'a [IncrementalSnapshotNodeV0],
1410    hashes_by_id: &BTreeMap<String, String>,
1411) -> BTreeMap<String, &'a IncrementalSnapshotNodeV0> {
1412    let mut unique = BTreeMap::new();
1413    let mut duplicates = BTreeSet::new();
1414    for node in nodes {
1415        let Some(hash) = hashes_by_id.get(node.id.as_str()) else {
1416            continue;
1417        };
1418        if unique.insert(hash.clone(), node).is_some() {
1419            duplicates.insert(hash.clone());
1420        }
1421    }
1422    for duplicate in duplicates {
1423        unique.remove(duplicate.as_str());
1424    }
1425    unique
1426}
1427
1428fn snapshot_node_value_matches(
1429    previous_node: &IncrementalSnapshotNodeV0,
1430    node: &IncrementalSnapshotNodeV0,
1431) -> bool {
1432    previous_node.digest == node.digest && previous_node.dependency_ids == node.dependency_ids
1433}
1434
1435fn compute_from_scratch_delta_dirty_ids(
1436    input: &IncrementalGraphInputV0,
1437    previous: Option<&IncrementalSnapshotV0>,
1438) -> BTreeSet<String> {
1439    let normalized_nodes = normalized_snapshot_nodes(input);
1440    let alpha_hashes_by_id = alpha_equivalence_hashes_by_node(input);
1441    let previous_alpha_hashes_by_id = previous
1442        .map(|snapshot| alpha_equivalence_hashes_by_snapshot_nodes(&snapshot.nodes))
1443        .unwrap_or_default();
1444    let previous_by_alpha_hash = previous
1445        .map(|snapshot| {
1446            unique_previous_nodes_by_alpha_hash(&snapshot.nodes, &previous_alpha_hashes_by_id)
1447        })
1448        .unwrap_or_default();
1449    let previous_by_id = previous
1450        .map(|snapshot| {
1451            snapshot
1452                .nodes
1453                .iter()
1454                .map(|node| (node.id.as_str(), node))
1455                .collect::<BTreeMap<_, _>>()
1456        })
1457        .unwrap_or_default();
1458    let mut dirty_ids = normalized_nodes
1459        .iter()
1460        .filter_map(|node| {
1461            let alpha_equivalence_hash = alpha_hashes_by_id
1462                .get(node.id.as_str())
1463                .map(String::as_str)
1464                .unwrap_or(node.id.as_str());
1465            let exact_previous = previous_by_id.get(node.id.as_str()).copied();
1466            let alpha_previous = previous_by_alpha_hash.get(alpha_equivalence_hash).copied();
1467            if exact_previous
1468                .filter(|previous_node| snapshot_node_value_matches(previous_node, node))
1469                .or(alpha_previous)
1470                .is_some()
1471            {
1472                return None;
1473            }
1474            Some(node.id.clone())
1475        })
1476        .collect::<BTreeSet<_>>();
1477
1478    let max_iterations = dependency_propagation_iteration_limit(normalized_nodes.len());
1479    for _ in 0..max_iterations {
1480        let mut changed = false;
1481        for node in &normalized_nodes {
1482            if dirty_ids.contains(node.id.as_str()) {
1483                continue;
1484            }
1485            if node
1486                .dependency_ids
1487                .iter()
1488                .any(|dependency_id| dirty_ids.contains(dependency_id.as_str()))
1489            {
1490                changed = dirty_ids.insert(node.id.clone()) || changed;
1491            }
1492        }
1493        if !changed {
1494            break;
1495        }
1496    }
1497
1498    dirty_ids
1499}
1500
1501fn stable_hash_hex(bytes: &[u8]) -> String {
1502    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
1503    for byte in bytes {
1504        hash ^= u64::from(*byte);
1505        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1506    }
1507    format!("{hash:016x}")
1508}
1509
1510fn dirty_set_is_dependency_closed(plan: &IncrementalComputationPlanV0) -> bool {
1511    let dirty_ids = plan
1512        .nodes
1513        .iter()
1514        .filter(|node| node.dirty)
1515        .map(|node| node.id.as_str())
1516        .collect::<BTreeSet<_>>();
1517    plan.nodes.iter().all(|node| {
1518        node.dirty
1519            || node
1520                .dependency_ids
1521                .iter()
1522                .all(|dependency_id| !dirty_ids.contains(dependency_id.as_str()))
1523    })
1524}
1525
1526fn generated_incremental_fuzz_graph(
1527    seed: u64,
1528    node_count: usize,
1529    changed_index: Option<usize>,
1530) -> IncrementalGraphInputV0 {
1531    let mut state = seed ^ 0xa076_1d64_78bd_642f;
1532    let nodes = (0..node_count)
1533        .map(|index| {
1534            let id = fuzz_node_id(index);
1535            let mut digest_seed = fuzz_next(&mut state);
1536            if changed_index == Some(index) {
1537                digest_seed ^= 0xffff_ffff_ffff_ffff;
1538            }
1539            let dependency_ids = (0..index)
1540                .filter(|candidate| {
1541                    let divisor = ((*candidate + 2) as u64).max(2);
1542                    (seed + index as u64).is_multiple_of(divisor)
1543                })
1544                .map(fuzz_node_id)
1545                .collect::<Vec<_>>();
1546            IncrementalNodeInputV0 {
1547                id,
1548                digest: format!("digest-{index}-{digest_seed:016x}"),
1549                dependency_ids,
1550            }
1551        })
1552        .collect();
1553
1554    IncrementalGraphInputV0 {
1555        revision: IncrementalRevisionV0 {
1556            value: 1 + if changed_index.is_some() { 1 } else { 0 },
1557        },
1558        nodes,
1559    }
1560}
1561
1562fn transitive_dependents(input: &IncrementalGraphInputV0, changed_id: &str) -> BTreeSet<String> {
1563    let mut dirty_ids = BTreeSet::from([changed_id.to_string()]);
1564    let max_iterations = dependency_propagation_iteration_limit(input.nodes.len());
1565    for _ in 0..max_iterations {
1566        let mut changed = false;
1567        for node in &input.nodes {
1568            if dirty_ids.contains(&node.id) {
1569                continue;
1570            }
1571            if node
1572                .dependency_ids
1573                .iter()
1574                .any(|dependency_id| dirty_ids.contains(dependency_id))
1575            {
1576                changed = dirty_ids.insert(node.id.clone()) || changed;
1577            }
1578        }
1579        if !changed {
1580            break;
1581        }
1582    }
1583    dirty_ids
1584}
1585
1586fn dependency_propagation_iteration_limit(node_count: usize) -> usize {
1587    node_count.saturating_add(1)
1588}
1589
1590fn fuzz_node_id(index: usize) -> String {
1591    format!("node-{index}")
1592}
1593
1594fn fuzz_next(state: &mut u64) -> u64 {
1595    *state = state
1596        .wrapping_mul(6_364_136_223_846_793_005)
1597        .wrapping_add(1_442_695_040_888_963_407);
1598    *state
1599}
1600
1601impl OmenaIncrementalDatabaseV0 {
1602    pub fn salsa_database(&self) -> &OmenaSalsaDatabaseV0 {
1603        &self.db
1604    }
1605
1606    pub fn node_input(&self, id: &str) -> Option<SalsaIncrementalNodeInputV0> {
1607        self.node_inputs_by_id.get(id).copied()
1608    }
1609
1610    pub fn graph_input(&self) -> Option<SalsaIncrementalGraphInputV0> {
1611        self.graph_input
1612    }
1613
1614    pub fn node_value(&self, id: &str) -> Option<String> {
1615        let graph = self.graph_input?;
1616        let node = self.node_input(id)?;
1617        Some(read_salsa_incremental_node_value(&self.db, graph, node))
1618    }
1619
1620    pub fn node_dirty_signature(&self, id: &str) -> Option<String> {
1621        let graph = self.graph_input?;
1622        let node = self.node_input(id)?;
1623        Some(read_salsa_incremental_node_dirty_signature(
1624            &self.db, graph, node,
1625        ))
1626    }
1627
1628    pub fn current_snapshot(&self) -> Option<&IncrementalSnapshotV0> {
1629        self.current_snapshot.as_ref()
1630    }
1631
1632    pub fn restore_snapshot(&mut self, snapshot: &IncrementalSnapshotV0) {
1633        let input = graph_input_from_snapshot(snapshot);
1634        self.upsert_graph_input(&input);
1635        self.current_snapshot = Some(snapshot.clone());
1636    }
1637
1638    pub fn plan_and_upsert_graph_input(
1639        &mut self,
1640        input: &IncrementalGraphInputV0,
1641    ) -> IncrementalDatabaseUpdateV0 {
1642        self.plan_and_upsert_graph_input_with_priority_inputs(input, &[])
1643    }
1644
1645    pub fn plan_and_upsert_graph_input_with_priority_inputs(
1646        &mut self,
1647        input: &IncrementalGraphInputV0,
1648        priority_inputs: &[IncrementalEditDistancePriorityInputV0],
1649    ) -> IncrementalDatabaseUpdateV0 {
1650        let previous_snapshot = self.current_snapshot.clone();
1651        let previous_signatures = self.dirty_signatures_for_snapshot(previous_snapshot.as_ref());
1652        let next_snapshot = self.upsert_graph_input(input);
1653        let incremental_plan = self.salsa_demand_plan_with_priority_inputs(
1654            input,
1655            previous_snapshot.as_ref(),
1656            previous_signatures,
1657            priority_inputs,
1658        );
1659        let datalog_rule_evaluator =
1660            summarize_datalog_rule_evaluator_for_plan_v0(input, incremental_plan.clone());
1661        self.current_snapshot = Some(next_snapshot.clone());
1662
1663        IncrementalDatabaseUpdateV0 {
1664            schema_version: "0",
1665            product: "omena-incremental.salsa-database-update",
1666            incremental_plan,
1667            datalog_rule_evaluator,
1668            next_snapshot,
1669        }
1670    }
1671
1672    fn dirty_signatures_for_snapshot(
1673        &self,
1674        snapshot: Option<&IncrementalSnapshotV0>,
1675    ) -> (BTreeMap<String, String>, BTreeMap<String, String>) {
1676        let Some(snapshot) = snapshot else {
1677            return (BTreeMap::new(), BTreeMap::new());
1678        };
1679        let Some(graph) = self.graph_input else {
1680            return (BTreeMap::new(), BTreeMap::new());
1681        };
1682        let alpha_hashes_by_id = alpha_equivalence_hashes_by_snapshot_nodes(&snapshot.nodes);
1683        let unique_previous_by_alpha =
1684            unique_previous_nodes_by_alpha_hash(&snapshot.nodes, &alpha_hashes_by_id);
1685        let by_id = snapshot
1686            .nodes
1687            .iter()
1688            .filter_map(|node| {
1689                let node_input = self.node_input(node.id.as_str())?;
1690                Some((
1691                    node.id.clone(),
1692                    read_salsa_incremental_node_dirty_signature(&self.db, graph, node_input),
1693                ))
1694            })
1695            .collect::<BTreeMap<_, _>>();
1696        let by_alpha_hash = unique_previous_by_alpha
1697            .into_iter()
1698            .filter_map(|(alpha_hash, node)| {
1699                by_id
1700                    .get(node.id.as_str())
1701                    .cloned()
1702                    .map(|signature| (alpha_hash, signature))
1703            })
1704            .collect::<BTreeMap<_, _>>();
1705        (by_id, by_alpha_hash)
1706    }
1707
1708    fn salsa_demand_plan_with_priority_inputs(
1709        &self,
1710        input: &IncrementalGraphInputV0,
1711        previous: Option<&IncrementalSnapshotV0>,
1712        previous_signatures: (BTreeMap<String, String>, BTreeMap<String, String>),
1713        priority_inputs: &[IncrementalEditDistancePriorityInputV0],
1714    ) -> IncrementalComputationPlanV0 {
1715        let normalized_nodes = normalized_snapshot_nodes(input);
1716        let alpha_hashes_by_id = alpha_equivalence_hashes_by_node(input);
1717        let alpha_equivalence_graph_hash = summarize_alpha_equivalence_hash(input);
1718        let previous_alpha_hashes_by_id = previous
1719            .map(|snapshot| alpha_equivalence_hashes_by_snapshot_nodes(&snapshot.nodes))
1720            .unwrap_or_default();
1721        let previous_by_alpha_hash = previous
1722            .map(|snapshot| {
1723                unique_previous_nodes_by_alpha_hash(&snapshot.nodes, &previous_alpha_hashes_by_id)
1724            })
1725            .unwrap_or_default();
1726        let previous_by_id = previous
1727            .map(|snapshot| {
1728                snapshot
1729                    .nodes
1730                    .iter()
1731                    .map(|node| (node.id.as_str(), node))
1732                    .collect::<BTreeMap<_, _>>()
1733            })
1734            .unwrap_or_default();
1735        let current_ids = normalized_nodes
1736            .iter()
1737            .map(|node| node.id.as_str())
1738            .collect::<BTreeSet<_>>();
1739        let current_alpha_hashes = alpha_hashes_by_id
1740            .values()
1741            .map(String::as_str)
1742            .collect::<BTreeSet<_>>();
1743        let removed_node_count = previous_by_id
1744            .keys()
1745            .filter(|id| {
1746                if current_ids.contains(**id) {
1747                    return false;
1748                }
1749                previous_alpha_hashes_by_id
1750                    .get(**id)
1751                    .is_none_or(|hash| !current_alpha_hashes.contains(hash.as_str()))
1752            })
1753            .count();
1754        let (previous_signature_by_id, previous_signature_by_alpha_hash) = previous_signatures;
1755        let mut dirty_ids = BTreeSet::<String>::new();
1756        let nodes = normalized_nodes
1757            .into_iter()
1758            .map(|node| {
1759                let alpha_equivalence_hash = alpha_hashes_by_id
1760                    .get(node.id.as_str())
1761                    .cloned()
1762                    .unwrap_or_else(|| stable_hash_hex(node.id.as_bytes()));
1763                let exact_previous = previous_by_id.get(node.id.as_str()).copied();
1764                let alpha_previous = previous_by_alpha_hash
1765                    .get(alpha_equivalence_hash.as_str())
1766                    .copied();
1767                let previous_value_match = exact_previous
1768                    .filter(|previous_node| snapshot_node_value_matches(previous_node, &node))
1769                    .or(alpha_previous);
1770                let current_signature = self.node_dirty_signature(node.id.as_str());
1771                let previous_signature = previous_value_match
1772                    .and_then(|previous_node| {
1773                        previous_signature_by_id.get(previous_node.id.as_str())
1774                    })
1775                    .or_else(|| {
1776                        exact_previous.and_then(|previous_node| {
1777                            previous_signature_by_id.get(previous_node.id.as_str())
1778                        })
1779                    })
1780                    .or_else(|| {
1781                        previous_signature_by_alpha_hash.get(alpha_equivalence_hash.as_str())
1782                    });
1783                let dependency_signature_equal = current_signature
1784                    .as_ref()
1785                    .zip(previous_signature)
1786                    .is_some_and(|(current, previous)| current == previous);
1787                let value_equal_to_previous = previous_value_match.is_some();
1788                let mut reasons = Vec::new();
1789                match previous_value_match.or(exact_previous) {
1790                    None => reasons.push("newNode"),
1791                    Some(previous_node) => {
1792                        if previous_value_match.is_none() {
1793                            if previous_node.digest != node.digest {
1794                                reasons.push("inputDigestChanged");
1795                            }
1796                            if previous_node.dependency_ids != node.dependency_ids {
1797                                reasons.push("dependencySetChanged");
1798                            }
1799                        }
1800                    }
1801                }
1802                if !dependency_signature_equal && reasons.is_empty() {
1803                    reasons.push("dependencyDirty");
1804                }
1805                let changed_at = if value_equal_to_previous {
1806                    previous
1807                        .map(|snapshot| snapshot.revision)
1808                        .unwrap_or(input.revision)
1809                } else {
1810                    input.revision
1811                };
1812                let dirty = !dependency_signature_equal;
1813                if dirty {
1814                    dirty_ids.insert(node.id.clone());
1815                }
1816
1817                IncrementalComputationNodeV0 {
1818                    alpha_equivalence_hash,
1819                    id: node.id,
1820                    digest: node.digest,
1821                    dependency_ids: node.dependency_ids,
1822                    dirty,
1823                    reasons,
1824                    changed_at,
1825                    verified_at: input.revision,
1826                    value_equal_to_previous,
1827                }
1828            })
1829            .collect::<Vec<_>>();
1830        let shadow_delta_oracle =
1831            summarize_incremental_shadow_delta_oracle_v0(input, previous, dirty_ids);
1832        let invalidation_priority_plan =
1833            summarize_incremental_invalidation_priority_plan_v0(&nodes, priority_inputs);
1834
1835        IncrementalComputationPlanV0 {
1836            schema_version: "0",
1837            product: "omena-incremental.computation-plan",
1838            revision: input.revision,
1839            node_count: nodes.len(),
1840            dirty_node_count: nodes.iter().filter(|node| node.dirty).count(),
1841            changed_input_count: nodes
1842                .iter()
1843                .filter(|node| node.reasons.contains(&"inputDigestChanged"))
1844                .count(),
1845            new_node_count: nodes
1846                .iter()
1847                .filter(|node| node.reasons.contains(&"newNode"))
1848                .count(),
1849            removed_node_count,
1850            dependency_dirty_count: nodes
1851                .iter()
1852                .filter(|node| node.reasons.contains(&"dependencyDirty"))
1853                .count(),
1854            alpha_equivalence_graph_hash,
1855            shadow_delta_oracle,
1856            invalidation_priority_plan,
1857            nodes,
1858        }
1859    }
1860
1861    pub fn upsert_graph_input(&mut self, input: &IncrementalGraphInputV0) -> IncrementalSnapshotV0 {
1862        let normalized_nodes = normalized_snapshot_nodes(input);
1863        let current_ids = normalized_nodes
1864            .iter()
1865            .map(|node| node.id.as_str())
1866            .collect::<BTreeSet<_>>();
1867        self.node_inputs_by_id
1868            .retain(|id, _node| current_ids.contains(id.as_str()));
1869
1870        for node in &normalized_nodes {
1871            self.upsert_node_input(node);
1872        }
1873        let graph_nodes = self.node_inputs_by_id.values().copied().collect::<Vec<_>>();
1874        self.sync_graph_input(graph_nodes);
1875
1876        let nodes = self
1877            .node_inputs_by_id
1878            .values()
1879            .copied()
1880            .map(|node| summarize_salsa_incremental_node_snapshot(&self.db, node))
1881            .collect::<Vec<_>>();
1882
1883        IncrementalSnapshotV0 {
1884            schema_version: "0",
1885            product: "omena-incremental.salsa-snapshot",
1886            revision: input.revision,
1887            nodes,
1888        }
1889    }
1890
1891    fn upsert_node_input(&mut self, node: &IncrementalSnapshotNodeV0) {
1892        let Some(node_input) = self.node_inputs_by_id.get(node.id.as_str()).copied() else {
1893            let node_input = SalsaIncrementalNodeInputV0::new(
1894                &self.db,
1895                node.id.clone(),
1896                node.digest.clone(),
1897                node.dependency_ids.clone(),
1898            );
1899            self.node_inputs_by_id.insert(node.id.clone(), node_input);
1900            return;
1901        };
1902
1903        if node_input.digest(&self.db).as_str() != node.digest.as_str() {
1904            node_input.set_digest(&mut self.db).to(node.digest.clone());
1905        }
1906        if node_input.dependency_ids(&self.db).as_slice() != node.dependency_ids.as_slice() {
1907            node_input
1908                .set_dependency_ids(&mut self.db)
1909                .to(node.dependency_ids.clone());
1910        }
1911    }
1912
1913    fn sync_graph_input(&mut self, nodes: Vec<SalsaIncrementalNodeInputV0>) {
1914        match self.graph_input {
1915            Some(graph) => {
1916                if graph.nodes(&self.db).as_slice() != nodes.as_slice() {
1917                    graph.set_nodes(&mut self.db).to(nodes);
1918                }
1919            }
1920            None => {
1921                self.graph_input = Some(SalsaIncrementalGraphInputV0::new(&self.db, nodes));
1922            }
1923        }
1924    }
1925}
1926
1927impl Default for IncrementalCancellationRegistryV0 {
1928    fn default() -> Self {
1929        Self::with_limit(DEFAULT_INCREMENTAL_CANCELLATION_LIMIT)
1930    }
1931}
1932
1933impl IncrementalCancellationRegistryV0 {
1934    pub fn with_limit(limit: usize) -> Self {
1935        Self {
1936            limit: limit.max(1),
1937            cancelled_request_ids: BTreeSet::new(),
1938        }
1939    }
1940
1941    pub fn cancel(&mut self, request_id: impl Into<String>) {
1942        if self.cancelled_request_ids.len() >= self.limit {
1943            self.cancelled_request_ids.clear();
1944        }
1945        self.cancelled_request_ids.insert(request_id.into());
1946    }
1947
1948    pub fn take_cancelled(&mut self, request_id: &str) -> bool {
1949        self.cancelled_request_ids.remove(request_id)
1950    }
1951
1952    pub fn take_cancelled_result(&mut self, request_id: &str) -> Result<(), salsa::Cancelled> {
1953        if self.take_cancelled(request_id) {
1954            Err(salsa::Cancelled::Local)
1955        } else {
1956            Ok(())
1957        }
1958    }
1959
1960    pub fn len(&self) -> usize {
1961        self.cancelled_request_ids.len()
1962    }
1963
1964    pub fn is_empty(&self) -> bool {
1965        self.cancelled_request_ids.is_empty()
1966    }
1967
1968    pub fn snapshot(&self) -> IncrementalCancellationSnapshotV0 {
1969        IncrementalCancellationSnapshotV0 {
1970            schema_version: "0",
1971            product: "omena-incremental.cancellation-registry",
1972            cancelled_request_count: self.cancelled_request_ids.len(),
1973            cancelled_request_ids: self.cancelled_request_ids.iter().cloned().collect(),
1974        }
1975    }
1976}
1977
1978#[cfg(test)]
1979mod tests {
1980    use super::{
1981        GuaranteeKindV0, IncrementalCancellationRegistryV0, IncrementalGraphInputV0,
1982        IncrementalNodeInputV0, IncrementalRevisionV0, OmenaIncrementalDatabaseV0,
1983        OmenaSalsaDatabaseV0, OmenaWorkspaceSnapshotIdV0, SalsaIncrementalNodeInputV0,
1984        read_salsa_incremental_node_dependency_ids, read_salsa_incremental_node_digest,
1985        read_salsa_transitive_c, read_salsa_transitive_unrelated,
1986        reset_salsa_node_value_query_runs, salsa_node_value_query_runs, snapshot_from_graph_input,
1987        summarize_datalog_rule_evaluator_v0, summarize_incremental_layer_evidence_v0,
1988        summarize_omena_incremental_boundary,
1989    };
1990    use omena_evidence_graph::GuaranteeFamilyV0;
1991    use omena_testkit::{InstrumentationSessionV0, with_instrumentation_session};
1992    use salsa::Setter;
1993    use std::collections::BTreeSet;
1994
1995    #[test]
1996    fn summarizes_incremental_boundary() {
1997        let summary = summarize_omena_incremental_boundary();
1998
1999        assert_eq!(summary.product, "omena-incremental.boundary");
2000        assert_eq!(
2001            summary.query_model,
2002            "salsaInput+trackedQueryFieldGranularReuse"
2003        );
2004        assert_eq!(
2005            summary.dependency_propagation_policy,
2006            "salsaDemandDirtySignatureReads"
2007        );
2008        assert_eq!(
2009            summary.maximum_dependency_propagation_iterations,
2010            "oracleOnlyNodeCount+1"
2011        );
2012        assert!(summary.dirty_reasons.contains(&"dependencyDirty"));
2013        assert!(
2014            summary
2015                .ready_surfaces
2016                .contains(&"incrementalCancellationRegistry")
2017        );
2018        assert!(summary.ready_surfaces.contains(&"datalogRuleEvaluatorV0"));
2019        assert!(
2020            summary
2021                .ready_surfaces
2022                .contains(&"salsaTrackedNodeSnapshotQuery")
2023        );
2024        assert!(
2025            summary
2026                .ready_surfaces
2027                .contains(&"salsaDemandDependencyReads")
2028        );
2029    }
2030
2031    #[test]
2032    fn workspace_snapshot_id_rekeys_incremental_revision() {
2033        let first_revision = IncrementalRevisionV0 { value: 7 };
2034        let same_revision = IncrementalRevisionV0 { value: 7 };
2035        let next_revision = IncrementalRevisionV0 { value: 8 };
2036
2037        let first_id = OmenaWorkspaceSnapshotIdV0::from_revision(first_revision);
2038        let same_id = OmenaWorkspaceSnapshotIdV0::from_revision(same_revision);
2039        let next_id = OmenaWorkspaceSnapshotIdV0::from_revision(next_revision);
2040
2041        assert_eq!(first_id, same_id);
2042        assert_ne!(first_id, next_id);
2043        assert_eq!(first_id.revision(), first_revision);
2044    }
2045
2046    #[test]
2047    fn first_plan_marks_all_nodes_dirty() {
2048        let input = sample_input("a:v1", "b:v1", 1);
2049        let plan = plan_from_database(&input, None);
2050
2051        assert_eq!(plan.product, "omena-incremental.computation-plan");
2052        assert_eq!(plan.node_count, 2);
2053        assert_eq!(plan.dirty_node_count, 2);
2054        assert_eq!(plan.new_node_count, 2);
2055    }
2056
2057    #[test]
2058    fn unchanged_second_plan_marks_nodes_clean() {
2059        let input = sample_input("a:v1", "b:v1", 1);
2060        let snapshot = snapshot_from_graph_input(&input);
2061        let next_input = sample_input("a:v1", "b:v1", 2);
2062        let plan = plan_from_database(&next_input, Some(&snapshot));
2063
2064        assert_eq!(plan.dirty_node_count, 0);
2065        assert_eq!(plan.changed_input_count, 0);
2066        assert_eq!(
2067            plan.shadow_delta_oracle.incremental_dirty_ids,
2068            Vec::<String>::new()
2069        );
2070        assert!(
2071            plan.shadow_delta_oracle
2072                .incremental_matches_from_scratch_delta
2073        );
2074        let Some(b) = node_by_id(&plan, "b") else {
2075            assert!(plan.nodes.iter().any(|node| node.id == "b"));
2076            return;
2077        };
2078        assert_eq!(b.changed_at.value, 1);
2079        assert_eq!(b.verified_at.value, 2);
2080        assert!(b.value_equal_to_previous);
2081    }
2082
2083    #[test]
2084    fn changed_dependency_marks_dependent_dirty() {
2085        let input = sample_input("a:v1", "b:v1", 1);
2086        let snapshot = snapshot_from_graph_input(&input);
2087        let next_input = sample_input("a:v2", "b:v1", 2);
2088        let plan = plan_from_database(&next_input, Some(&snapshot));
2089
2090        assert_eq!(plan.changed_input_count, 1);
2091        assert_eq!(plan.dependency_dirty_count, 1);
2092        assert_eq!(node_reasons(&plan, "a"), vec!["inputDigestChanged"]);
2093        assert_eq!(node_reasons(&plan, "b"), vec!["dependencyDirty"]);
2094        assert_eq!(
2095            plan.shadow_delta_oracle.incremental_dirty_ids,
2096            vec!["a".to_string(), "b".to_string()]
2097        );
2098        assert_eq!(
2099            plan.shadow_delta_oracle.from_scratch_dirty_ids,
2100            plan.shadow_delta_oracle.incremental_dirty_ids
2101        );
2102        assert!(
2103            plan.shadow_delta_oracle
2104                .incremental_matches_from_scratch_delta
2105        );
2106
2107        let Some(changed) = node_by_id(&plan, "a") else {
2108            assert!(plan.nodes.iter().any(|node| node.id == "a"));
2109            return;
2110        };
2111        assert_eq!(changed.changed_at.value, 2);
2112        assert_eq!(changed.verified_at.value, 2);
2113        assert!(!changed.value_equal_to_previous);
2114
2115        let Some(backdated) = node_by_id(&plan, "b") else {
2116            assert!(plan.nodes.iter().any(|node| node.id == "b"));
2117            return;
2118        };
2119        assert_eq!(backdated.changed_at.value, 1);
2120        assert_eq!(backdated.verified_at.value, 2);
2121        assert!(backdated.value_equal_to_previous);
2122    }
2123
2124    #[test]
2125    fn alpha_equivalence_hash_ignores_fixture_node_renaming() {
2126        let left = sample_input("root:v1", "leaf:v1", 1);
2127        let right = IncrementalGraphInputV0 {
2128            revision: IncrementalRevisionV0 { value: 1 },
2129            nodes: vec![
2130                IncrementalNodeInputV0 {
2131                    id: "renamed-leaf".to_string(),
2132                    digest: "leaf:v1".to_string(),
2133                    dependency_ids: vec!["renamed-root".to_string()],
2134                },
2135                IncrementalNodeInputV0 {
2136                    id: "renamed-root".to_string(),
2137                    digest: "root:v1".to_string(),
2138                    dependency_ids: Vec::new(),
2139                },
2140            ],
2141        };
2142        let left_plan = plan_from_database(&left, None);
2143        let right_plan = plan_from_database(&right, None);
2144
2145        assert_eq!(
2146            left_plan.alpha_equivalence_graph_hash.product,
2147            "omena-incremental.alpha-equivalence-hash"
2148        );
2149        assert_eq!(
2150            left_plan.alpha_equivalence_graph_hash.feature_gate,
2151            "incremental-alpha-equivalence-hash-v0"
2152        );
2153        assert!(!left_plan.alpha_equivalence_graph_hash.theorem_claimed);
2154        assert_eq!(
2155            left_plan.alpha_equivalence_graph_hash.hash,
2156            right_plan.alpha_equivalence_graph_hash.hash
2157        );
2158    }
2159
2160    #[test]
2161    fn preceding_sibling_insert_keeps_shifted_nodes_clean_by_alpha_hash() {
2162        let previous = IncrementalGraphInputV0 {
2163            revision: IncrementalRevisionV0 { value: 1 },
2164            nodes: vec![
2165                IncrementalNodeInputV0 {
2166                    id: "path:0".to_string(),
2167                    digest: "button:v1".to_string(),
2168                    dependency_ids: Vec::new(),
2169                },
2170                IncrementalNodeInputV0 {
2171                    id: "path:1".to_string(),
2172                    digest: "card:v1".to_string(),
2173                    dependency_ids: Vec::new(),
2174                },
2175            ],
2176        };
2177        let previous_snapshot = snapshot_from_graph_input(&previous);
2178        let next = IncrementalGraphInputV0 {
2179            revision: IncrementalRevisionV0 { value: 2 },
2180            nodes: vec![
2181                IncrementalNodeInputV0 {
2182                    id: "path:0".to_string(),
2183                    digest: "import:v1".to_string(),
2184                    dependency_ids: Vec::new(),
2185                },
2186                IncrementalNodeInputV0 {
2187                    id: "path:1".to_string(),
2188                    digest: "button:v1".to_string(),
2189                    dependency_ids: Vec::new(),
2190                },
2191                IncrementalNodeInputV0 {
2192                    id: "path:2".to_string(),
2193                    digest: "card:v1".to_string(),
2194                    dependency_ids: Vec::new(),
2195                },
2196            ],
2197        };
2198        let plan = plan_from_database(&next, Some(&previous_snapshot));
2199        let dirty_ids = plan
2200            .nodes
2201            .iter()
2202            .filter(|node| node.dirty)
2203            .map(|node| node.id.as_str())
2204            .collect::<Vec<_>>();
2205
2206        assert_eq!(dirty_ids, vec!["path:0"]);
2207        assert!(node_by_id(&plan, "path:1").is_some_and(|node| node.value_equal_to_previous));
2208        assert!(node_by_id(&plan, "path:2").is_some_and(|node| node.value_equal_to_previous));
2209    }
2210
2211    #[test]
2212    fn incremental_shadow_delta_oracle_matches_from_scratch_delta() {
2213        let previous = sample_input("a:v1", "b:v1", 1);
2214        let previous_snapshot = snapshot_from_graph_input(&previous);
2215        let next = sample_input("a:v2", "b:v1", 2);
2216        let plan = plan_from_database(&next, Some(&previous_snapshot));
2217
2218        assert_eq!(
2219            plan.shadow_delta_oracle.product,
2220            "omena-incremental.shadow-delta-oracle"
2221        );
2222        assert_eq!(
2223            plan.shadow_delta_oracle.feature_gate,
2224            "incremental-shadow-delta-v0"
2225        );
2226        assert_eq!(
2227            plan.shadow_delta_oracle.claim_level,
2228            "sampledFixtureWitnessNotEquivalenceProof"
2229        );
2230        assert!(!plan.shadow_delta_oracle.theorem_claimed);
2231        assert_eq!(
2232            plan.shadow_delta_oracle.incremental_dirty_ids,
2233            vec!["a".to_string(), "b".to_string()]
2234        );
2235        assert_eq!(
2236            plan.shadow_delta_oracle.incremental_dirty_ids,
2237            plan.shadow_delta_oracle.from_scratch_dirty_ids
2238        );
2239        assert!(
2240            plan.shadow_delta_oracle
2241                .incremental_matches_from_scratch_delta
2242        );
2243        assert!(plan.shadow_delta_oracle.sampled_shadow_witness_ready);
2244        assert!(!plan.shadow_delta_oracle.dbsp_zset_claim_ready);
2245        assert!(!plan.shadow_delta_oracle.performance_benchmark_claim_ready);
2246    }
2247
2248    #[test]
2249    fn edit_distance_priority_orders_dirty_nodes_for_scheduler() {
2250        let previous = three_node_input("a:v1", "b:v1", "c:v1", 1);
2251        let previous_snapshot = snapshot_from_graph_input(&previous);
2252        let next = three_node_input("a:v2", "b:v2", "c:v1", 2);
2253        let plan = plan_from_database_with_priority_inputs(
2254            &next,
2255            Some(&previous_snapshot),
2256            &[
2257                priority_input("a", 1, 1, true),
2258                priority_input("b", 8, 2, true),
2259            ],
2260        );
2261
2262        assert_eq!(
2263            plan.invalidation_priority_plan.product,
2264            "omena-incremental.invalidation-priority-plan"
2265        );
2266        assert_eq!(
2267            plan.invalidation_priority_plan.feature_gate,
2268            "incremental-edit-distance-priority-v0"
2269        );
2270        assert_eq!(
2271            plan.invalidation_priority_plan.calibration_stage,
2272            "fixtureWitnessDistanceMarginWeightedV0"
2273        );
2274        assert!(!plan.invalidation_priority_plan.theorem_claimed);
2275        assert!(!plan.invalidation_priority_plan.public_safety_claim_ready);
2276        assert_eq!(plan.invalidation_priority_plan.metric_input_count, 2);
2277        assert_eq!(plan.invalidation_priority_plan.metric_consumed_count, 2);
2278        assert_eq!(
2279            plan.invalidation_priority_plan.prioritized_dirty_node_ids,
2280            vec!["b".to_string(), "a".to_string(), "c".to_string()]
2281        );
2282        let first = &plan.invalidation_priority_plan.entries[0];
2283        assert_eq!(first.node_id, "b");
2284        assert_eq!(first.priority_rank, 1);
2285        assert!(first.metric_consumed);
2286        assert_eq!(first.edit_distance_total, Some(8));
2287        assert_eq!(first.cascade_margin_abs_distance, Some(2));
2288        assert!(first.bridge_checked);
2289    }
2290
2291    #[test]
2292    fn datalog_rule_evaluator_contract_matches_incremental_dirty_plan() {
2293        let input = sample_input("a:v1", "b:v1", 1);
2294        let snapshot = snapshot_from_graph_input(&input);
2295        let next_input = sample_input("a:v2", "b:v1", 2);
2296        let summary = summarize_datalog_rule_evaluator_v0(&next_input, Some(&snapshot));
2297
2298        assert_eq!(summary.schema_version, "0");
2299        assert_eq!(summary.product, "omena-incremental.datalog-rule-evaluator");
2300        assert_eq!(summary.evaluator_kind, "typedContractOverSalsaDemandPlan");
2301        assert_eq!(
2302            summary.substrate,
2303            "omena-incremental.salsa-backed-computation-plan"
2304        );
2305        assert!(!summary.external_host_ready);
2306        assert_eq!(summary.rule_count, summary.rules.len());
2307        assert_eq!(summary.relation_count, summary.relations.len());
2308        assert_eq!(summary.input_node_count, 2);
2309        assert_eq!(summary.dirty_node_count, 2);
2310        assert_eq!(summary.derived_node_count, 1);
2311        assert_eq!(summary.iteration_limit, 3);
2312        assert!(summary.fixed_point_reached);
2313        assert_eq!(summary.incremental_plan.changed_input_count, 1);
2314        assert_eq!(summary.incremental_plan.dependency_dirty_count, 1);
2315        assert!(summary.rules.iter().any(|rule| {
2316            rule.name == "dependencyDirtyDemandRead"
2317                && rule.body == vec!["dependsOn(Node,Dependency)", "dirty(Dependency)"]
2318        }));
2319    }
2320
2321    #[test]
2322    fn datalog_rule_evaluator_fixture_corpus_matches_incremental_fixed_point() {
2323        for seed in [1, 2, 3, 5, 8, 13, 21, 34] {
2324            let previous_input = super::generated_incremental_fuzz_graph(seed, 8, None);
2325            let previous_snapshot = snapshot_from_graph_input(&previous_input);
2326            let next_input = super::generated_incremental_fuzz_graph(seed, 8, Some(3));
2327            let plan = plan_from_database(&next_input, Some(&previous_snapshot));
2328            let summary =
2329                summarize_datalog_rule_evaluator_v0(&next_input, Some(&previous_snapshot));
2330
2331            assert_eq!(summary.incremental_plan, plan);
2332            assert_eq!(summary.dirty_node_count, plan.dirty_node_count);
2333            assert_eq!(summary.derived_node_count, plan.dependency_dirty_count);
2334            assert!(summary.fixed_point_reached);
2335            assert!(!summary.external_host_ready);
2336            assert_eq!(summary.rule_count, 4);
2337            assert_eq!(summary.relation_count, 5);
2338        }
2339    }
2340
2341    #[test]
2342    fn cyclic_dependency_graph_uses_bounded_dirty_propagation() {
2343        let input = cyclic_input("a:v1", "b:v1", 1);
2344        let snapshot = snapshot_from_graph_input(&input);
2345        let next_input = cyclic_input("a:v2", "b:v1", 2);
2346        let plan = plan_from_database(&next_input, Some(&snapshot));
2347
2348        assert_eq!(plan.changed_input_count, 1);
2349        assert_eq!(plan.dirty_node_count, 2);
2350        assert_eq!(node_reasons(&plan, "a"), vec!["inputDigestChanged"]);
2351        assert_eq!(node_reasons(&plan, "b"), vec!["dependencyDirty"]);
2352        assert_eq!(
2353            super::dependency_propagation_iteration_limit(input.nodes.len()),
2354            input.nodes.len() + 1
2355        );
2356    }
2357
2358    #[test]
2359    fn fuzz_seed_corpus_preserves_incremental_dirty_set_invariants() {
2360        let report = super::run_incremental_fuzz_seed_corpus();
2361
2362        assert_eq!(report.product, "omena-incremental.fuzz-seed-corpus");
2363        assert_eq!(report.failed_count, 0);
2364        assert_eq!(report.passed_count, report.case_count);
2365        assert!(
2366            report
2367                .results
2368                .iter()
2369                .any(|result| result.changed_node_id.is_none())
2370        );
2371        assert!(
2372            report
2373                .results
2374                .iter()
2375                .any(|result| result.expected_dirty_node_count > 1)
2376        );
2377    }
2378
2379    #[test]
2380    fn m6_incremental_layer_evidence_is_limited_to_real_invalidation_layer() {
2381        let evidence = summarize_incremental_layer_evidence_v0();
2382
2383        assert_eq!(evidence.schema_version, "0");
2384        assert_eq!(evidence.product, "omena-incremental.layer-evidence");
2385        assert_eq!(evidence.claim_level, "m6IncrementalLayerEvidenceOnly");
2386        assert_eq!(
2387            evidence.invalidation_layer,
2388            "stableNodeIdDigestDependencyGraph"
2389        );
2390        assert!(evidence.real_invalidation_evidence_ready);
2391        assert!(evidence.fuzz_evidence_ready);
2392        assert!(evidence.salsa_reuse_evidence_ready);
2393        assert!(evidence.datalog_contract_evidence_ready);
2394        assert!(evidence.value_equality_backdating_ready);
2395        assert!(evidence.alpha_equivalence_hash_ready);
2396        assert!(evidence.shadow_delta_oracle_ready);
2397        assert!(evidence.edit_distance_priority_ready);
2398        assert!(evidence.benchmark_surface_ready);
2399        assert!(!evidence.performance_benchmark_claim_ready);
2400        assert!(!evidence.external_datalog_host_ready);
2401        assert!(!evidence.dbsp_zset_claim_ready);
2402        assert!(!evidence.public_safety_claim_ready);
2403        assert_eq!(
2404            evidence.benchmark_gate,
2405            "rust/z5-performance-baseline-readiness"
2406        );
2407        assert_eq!(evidence.fuzz_report.failed_count, 0);
2408        assert_eq!(
2409            evidence.sample_update.incremental_plan.changed_input_count,
2410            1
2411        );
2412        assert_eq!(
2413            evidence
2414                .sample_update
2415                .incremental_plan
2416                .dependency_dirty_count,
2417            1
2418        );
2419        assert!(
2420            evidence
2421                .supported_claims
2422                .contains(&"dependency dirty-set fixed point")
2423        );
2424        assert!(
2425            evidence
2426                .supported_claims
2427                .contains(&"value-equality backdating with changed_at/verified_at split")
2428        );
2429        assert!(
2430            evidence
2431                .supported_claims
2432                .contains(&"edit-distance weighted invalidation priority")
2433        );
2434        assert!(evidence.deferred_claims.contains(&"DBSP runtime"));
2435        assert!(
2436            evidence
2437                .deferred_claims
2438                .contains(&"Z-set differential dataflow semantics")
2439        );
2440    }
2441
2442    #[test]
2443    fn incremental_claim_levels_round_trip_to_guarantee_kinds() {
2444        let evidence = summarize_incremental_layer_evidence_v0();
2445        let plan = &evidence.sample_update.incremental_plan;
2446        let priority_input = priority_input("style", 3, 2, true);
2447
2448        for claim_level in [
2449            evidence.claim_level,
2450            plan.alpha_equivalence_graph_hash.claim_level,
2451            plan.shadow_delta_oracle.claim_level,
2452            plan.invalidation_priority_plan.claim_level,
2453            priority_input.claim_level,
2454        ] {
2455            assert_eq!(
2456                GuaranteeKindV0::from_existing_label(claim_level)
2457                    .and_then(GuaranteeKindV0::existing_label),
2458                Some(claim_level)
2459            );
2460        }
2461    }
2462
2463    #[test]
2464    fn shadow_delta_fixture_stamp_refuses_mismatch_and_flag_disagreement() {
2465        let oracle = |incremental: &[&str], from_scratch: &[&str], reported_match| {
2466            super::IncrementalShadowDeltaOracleV0 {
2467                schema_version: "0",
2468                product: "omena-incremental.shadow-delta-oracle",
2469                feature_gate: "incremental-shadow-delta-v0",
2470                claim_level: "sampledFixtureWitnessNotEquivalenceProof",
2471                theorem_claimed: false,
2472                sampled_shadow_witness_ready: reported_match,
2473                incremental_dirty_ids: incremental
2474                    .iter()
2475                    .map(|value| (*value).to_string())
2476                    .collect(),
2477                from_scratch_dirty_ids: from_scratch
2478                    .iter()
2479                    .map(|value| (*value).to_string())
2480                    .collect(),
2481                incremental_matches_from_scratch_delta: reported_match,
2482                dbsp_zset_claim_ready: false,
2483                performance_benchmark_claim_ready: false,
2484            }
2485        };
2486
2487        let recomputed_mismatch = oracle(&["source", "style"], &["source"], false);
2488        assert_eq!(
2489            recomputed_mismatch.evidence_node_seed().earned_via,
2490            GuaranteeFamilyV0::FloorAssumption
2491        );
2492
2493        let lying_match_flag = oracle(&["source", "style"], &["source"], true);
2494        assert_eq!(
2495            lying_match_flag.evidence_node_seed().earned_via,
2496            GuaranteeFamilyV0::FloorAssumption
2497        );
2498
2499        let lying_mismatch_flag = oracle(&["source", "style"], &["source", "style"], false);
2500        assert_eq!(
2501            lying_mismatch_flag.evidence_node_seed().earned_via,
2502            GuaranteeFamilyV0::FloorAssumption
2503        );
2504
2505        let consistent = oracle(&["source", "style"], &["source", "style"], true);
2506        assert_eq!(
2507            consistent.evidence_node_seed().earned_via,
2508            GuaranteeFamilyV0::SampledFixtureCorpus
2509        );
2510    }
2511
2512    #[test]
2513    fn incremental_layer_evidence_graph_preserves_public_shape() -> Result<(), String> {
2514        let evidence = summarize_incremental_layer_evidence_v0();
2515        let before = serde_json::to_value(&evidence).map_err(|error| error.to_string())?;
2516        let graph = evidence
2517            .evidence_graph()
2518            .map_err(|error| format!("{error:?}"))?;
2519        let after = serde_json::to_value(&evidence).map_err(|error| error.to_string())?;
2520
2521        assert_eq!(before, after);
2522        assert_eq!(graph.nodes.len(), 4);
2523        assert_eq!(graph.edges.len(), 4);
2524        let labels = graph
2525            .nodes
2526            .iter()
2527            .map(|node| node.guarantee.existing_label())
2528            .collect::<Vec<_>>();
2529        assert!(labels.contains(&Some("m6IncrementalLayerEvidenceOnly")));
2530        assert!(labels.contains(&Some("fixtureWitnessAlphaRenamingStableHash")));
2531        assert!(labels.contains(&Some("sampledFixtureWitnessNotEquivalenceProof")));
2532        assert!(labels.contains(&Some("fixtureWitnessSchedulerPriority")));
2533        for node in &graph.nodes {
2534            let expected_family = match node.guarantee {
2535                GuaranteeKindV0::SampledFixtureWitness => GuaranteeFamilyV0::SampledFixtureCorpus,
2536                GuaranteeKindV0::AlphaRenamingStableHashFixtureWitness
2537                | GuaranteeKindV0::SchedulerPriorityFixtureWitness
2538                | GuaranteeKindV0::IncrementalLayerEvidenceOnly => {
2539                    GuaranteeFamilyV0::FloorAssumption
2540                }
2541                guarantee => {
2542                    return Err(format!(
2543                        "unexpected incremental evidence guarantee {guarantee:?}"
2544                    ));
2545                }
2546            };
2547            assert_eq!(
2548                node.earned_via(),
2549                expected_family,
2550                "node {:?} pairs guarantee {:?} with an unsupported earned family",
2551                node.key,
2552                node.guarantee,
2553            );
2554        }
2555        assert_eq!(
2556            priority_input("style", 3, 2, true)
2557                .evidence_node_seed()
2558                .earned_via,
2559            GuaranteeFamilyV0::FloorAssumption
2560        );
2561        Ok(())
2562    }
2563
2564    #[test]
2565    fn salsa_database_reuses_digest_query_when_only_dependencies_change() {
2566        let session = InstrumentationSessionV0::default();
2567        with_instrumentation_session(session.clone(), || {
2568            session.reset_salsa_query_run_counts();
2569
2570            let mut db = OmenaIncrementalDatabaseV0::default();
2571            let input = IncrementalGraphInputV0 {
2572                revision: IncrementalRevisionV0 { value: 1 },
2573                nodes: vec![IncrementalNodeInputV0 {
2574                    id: "a".to_string(),
2575                    digest: "a:v1".to_string(),
2576                    dependency_ids: Vec::new(),
2577                }],
2578            };
2579            let snapshot = db.upsert_graph_input(&input);
2580            assert_eq!(snapshot.product, "omena-incremental.salsa-snapshot");
2581
2582            let Some(node) = db.node_input("a") else {
2583                return;
2584            };
2585            assert_eq!(
2586                read_salsa_incremental_node_digest(db.salsa_database(), node),
2587                "a:v1"
2588            );
2589            assert_eq!(
2590                read_salsa_incremental_node_dependency_ids(db.salsa_database(), node),
2591                Vec::<String>::new()
2592            );
2593            let counts = session.salsa_query_run_counts();
2594            assert_eq!(counts.digest, 1);
2595            assert_eq!(counts.dependency, 1);
2596
2597            let next_input = IncrementalGraphInputV0 {
2598                revision: IncrementalRevisionV0 { value: 2 },
2599                nodes: vec![IncrementalNodeInputV0 {
2600                    id: "a".to_string(),
2601                    digest: "a:v1".to_string(),
2602                    dependency_ids: vec!["root".to_string()],
2603                }],
2604            };
2605            db.upsert_graph_input(&next_input);
2606
2607            let Some(node) = db.node_input("a") else {
2608                return;
2609            };
2610            assert_eq!(
2611                read_salsa_incremental_node_digest(db.salsa_database(), node),
2612                "a:v1"
2613            );
2614            assert_eq!(
2615                read_salsa_incremental_node_dependency_ids(db.salsa_database(), node),
2616                vec!["root".to_string()]
2617            );
2618            let counts = session.salsa_query_run_counts();
2619            assert_eq!(counts.digest, 1);
2620            assert_eq!(counts.dependency, 2);
2621        });
2622    }
2623
2624    #[test]
2625    fn salsa_transitive_query_graph_matches_planner_dirty_set() {
2626        let session = InstrumentationSessionV0::default();
2627        with_instrumentation_session(session.clone(), || {
2628            session.reset_salsa_query_run_counts();
2629
2630            let mut db = OmenaSalsaDatabaseV0::new();
2631            let a = SalsaIncrementalNodeInputV0::new(
2632                &db,
2633                "a".to_string(),
2634                "a:v1".to_string(),
2635                Vec::new(),
2636            );
2637            let b = SalsaIncrementalNodeInputV0::new(
2638                &db,
2639                "b".to_string(),
2640                "b:v1".to_string(),
2641                vec!["a".to_string()],
2642            );
2643            let c = SalsaIncrementalNodeInputV0::new(
2644                &db,
2645                "c".to_string(),
2646                "c:v1".to_string(),
2647                vec!["b".to_string()],
2648            );
2649            let unrelated = SalsaIncrementalNodeInputV0::new(
2650                &db,
2651                "unrelated".to_string(),
2652                "u:v1".to_string(),
2653                Vec::new(),
2654            );
2655
2656            assert_eq!(
2657                read_salsa_transitive_c(&db, a, b, c),
2658                "a=a:v1|b=b:v1|c=c:v1"
2659            );
2660            assert_eq!(read_salsa_transitive_unrelated(&db, unrelated), "u=u:v1");
2661
2662            session.reset_salsa_query_run_counts();
2663
2664            a.set_digest(&mut db).to("a:v2".to_string());
2665
2666            assert_eq!(
2667                read_salsa_transitive_c(&db, a, b, c),
2668                "a=a:v2|b=b:v1|c=c:v1"
2669            );
2670            assert_eq!(read_salsa_transitive_unrelated(&db, unrelated), "u=u:v1");
2671
2672            let counts = session.salsa_query_run_counts();
2673            assert_eq!(counts.transitive_leaf, 1);
2674            assert_eq!(counts.transitive_a, 1);
2675            assert_eq!(counts.transitive_b, 1);
2676            assert_eq!(counts.transitive_c, 1);
2677            assert_eq!(counts.transitive_unrelated, 0);
2678
2679            let previous = IncrementalGraphInputV0 {
2680                revision: IncrementalRevisionV0 { value: 1 },
2681                nodes: vec![
2682                    IncrementalNodeInputV0 {
2683                        id: "a".to_string(),
2684                        digest: "a:v1".to_string(),
2685                        dependency_ids: Vec::new(),
2686                    },
2687                    IncrementalNodeInputV0 {
2688                        id: "b".to_string(),
2689                        digest: "b:v1".to_string(),
2690                        dependency_ids: vec!["a".to_string()],
2691                    },
2692                    IncrementalNodeInputV0 {
2693                        id: "c".to_string(),
2694                        digest: "c:v1".to_string(),
2695                        dependency_ids: vec!["b".to_string()],
2696                    },
2697                    IncrementalNodeInputV0 {
2698                        id: "unrelated".to_string(),
2699                        digest: "u:v1".to_string(),
2700                        dependency_ids: Vec::new(),
2701                    },
2702                ],
2703            };
2704            let next = IncrementalGraphInputV0 {
2705                revision: IncrementalRevisionV0 { value: 2 },
2706                nodes: vec![
2707                    IncrementalNodeInputV0 {
2708                        id: "a".to_string(),
2709                        digest: "a:v2".to_string(),
2710                        dependency_ids: Vec::new(),
2711                    },
2712                    IncrementalNodeInputV0 {
2713                        id: "b".to_string(),
2714                        digest: "b:v1".to_string(),
2715                        dependency_ids: vec!["a".to_string()],
2716                    },
2717                    IncrementalNodeInputV0 {
2718                        id: "c".to_string(),
2719                        digest: "c:v1".to_string(),
2720                        dependency_ids: vec!["b".to_string()],
2721                    },
2722                    IncrementalNodeInputV0 {
2723                        id: "unrelated".to_string(),
2724                        digest: "u:v1".to_string(),
2725                        dependency_ids: Vec::new(),
2726                    },
2727                ],
2728            };
2729            let previous_snapshot = snapshot_from_graph_input(&previous);
2730            let plan = plan_from_database(&next, Some(&previous_snapshot));
2731            let planner_dirty_ids = plan
2732                .nodes
2733                .iter()
2734                .filter(|node| node.dirty)
2735                .map(|node| node.id.as_str())
2736                .collect::<BTreeSet<_>>();
2737            let salsa_rerun_ids = ["a", "b", "c"].into_iter().collect::<BTreeSet<_>>();
2738
2739            assert_eq!(planner_dirty_ids, salsa_rerun_ids);
2740        });
2741    }
2742
2743    #[test]
2744    fn production_node_value_query_reads_only_transitive_dependencies() {
2745        let mut db = OmenaIncrementalDatabaseV0::default();
2746        let input = IncrementalGraphInputV0 {
2747            revision: IncrementalRevisionV0 { value: 1 },
2748            nodes: vec![
2749                IncrementalNodeInputV0 {
2750                    id: "a".to_string(),
2751                    digest: "a:v1".to_string(),
2752                    dependency_ids: Vec::new(),
2753                },
2754                IncrementalNodeInputV0 {
2755                    id: "b".to_string(),
2756                    digest: "b:v1".to_string(),
2757                    dependency_ids: vec!["a".to_string()],
2758                },
2759                IncrementalNodeInputV0 {
2760                    id: "c".to_string(),
2761                    digest: "c:v1".to_string(),
2762                    dependency_ids: vec!["b".to_string()],
2763                },
2764                IncrementalNodeInputV0 {
2765                    id: "unrelated".to_string(),
2766                    digest: "u:v1".to_string(),
2767                    dependency_ids: Vec::new(),
2768                },
2769            ],
2770        };
2771        db.upsert_graph_input(&input);
2772
2773        assert_eq!(
2774            db.node_value("c"),
2775            Some("c=c:v1;deps=[b=b:v1;deps=[a=a:v1;deps=[]]]".to_string())
2776        );
2777        assert_eq!(
2778            db.node_value("unrelated"),
2779            Some("unrelated=u:v1;deps=[]".to_string())
2780        );
2781
2782        reset_salsa_node_value_query_runs();
2783        let next = IncrementalGraphInputV0 {
2784            revision: IncrementalRevisionV0 { value: 2 },
2785            nodes: vec![
2786                IncrementalNodeInputV0 {
2787                    id: "a".to_string(),
2788                    digest: "a:v2".to_string(),
2789                    dependency_ids: Vec::new(),
2790                },
2791                IncrementalNodeInputV0 {
2792                    id: "b".to_string(),
2793                    digest: "b:v1".to_string(),
2794                    dependency_ids: vec!["a".to_string()],
2795                },
2796                IncrementalNodeInputV0 {
2797                    id: "c".to_string(),
2798                    digest: "c:v1".to_string(),
2799                    dependency_ids: vec!["b".to_string()],
2800                },
2801                IncrementalNodeInputV0 {
2802                    id: "unrelated".to_string(),
2803                    digest: "u:v1".to_string(),
2804                    dependency_ids: Vec::new(),
2805                },
2806            ],
2807        };
2808        db.upsert_graph_input(&next);
2809
2810        assert_eq!(
2811            db.node_value("c"),
2812            Some("c=c:v1;deps=[b=b:v1;deps=[a=a:v2;deps=[]]]".to_string())
2813        );
2814        assert_eq!(
2815            db.node_value("unrelated"),
2816            Some("unrelated=u:v1;deps=[]".to_string())
2817        );
2818        assert_eq!(salsa_node_value_query_runs("a"), 1);
2819        assert_eq!(salsa_node_value_query_runs("b"), 1);
2820        assert_eq!(salsa_node_value_query_runs("c"), 1);
2821        assert_eq!(salsa_node_value_query_runs("unrelated"), 0);
2822    }
2823
2824    #[test]
2825    fn salsa_database_update_owns_plan_and_snapshot_progression() {
2826        let mut db = OmenaIncrementalDatabaseV0::default();
2827        let input = sample_input("a:v1", "b:v1", 1);
2828        let first = db.plan_and_upsert_graph_input(&input);
2829
2830        assert_eq!(first.product, "omena-incremental.salsa-database-update");
2831        assert_eq!(first.incremental_plan.dirty_node_count, 2);
2832        assert_eq!(
2833            first.next_snapshot.product,
2834            "omena-incremental.salsa-snapshot"
2835        );
2836        assert!(db.current_snapshot().is_some());
2837
2838        let unchanged = db.plan_and_upsert_graph_input(&sample_input("a:v1", "b:v1", 2));
2839        assert_eq!(unchanged.incremental_plan.dirty_node_count, 0);
2840
2841        let changed = db.plan_and_upsert_graph_input(&sample_input("a:v2", "b:v1", 3));
2842        assert_eq!(changed.incremental_plan.changed_input_count, 1);
2843        assert_eq!(changed.incremental_plan.dependency_dirty_count, 1);
2844        assert_eq!(changed.datalog_rule_evaluator.revision.value, 3);
2845        assert_eq!(
2846            changed.datalog_rule_evaluator.incremental_plan,
2847            changed.incremental_plan
2848        );
2849        assert_eq!(changed.datalog_rule_evaluator.dirty_node_count, 2);
2850        assert_eq!(changed.datalog_rule_evaluator.derived_node_count, 1);
2851        assert!(changed.datalog_rule_evaluator.fixed_point_reached);
2852        assert!(!changed.datalog_rule_evaluator.external_host_ready);
2853    }
2854
2855    #[test]
2856    fn cancellation_registry_tracks_and_consumes_request_ids() {
2857        let mut registry = IncrementalCancellationRegistryV0::with_limit(4);
2858
2859        registry.cancel("s:hover-1");
2860
2861        assert_eq!(registry.len(), 1);
2862        assert!(matches!(
2863            registry.take_cancelled_result("s:hover-1"),
2864            Err(salsa::Cancelled::Local)
2865        ));
2866        assert!(matches!(
2867            registry.take_cancelled_result("s:hover-1"),
2868            Ok(())
2869        ));
2870        assert!(registry.is_empty());
2871    }
2872
2873    #[test]
2874    fn cancellation_registry_bounds_stale_cancelled_requests() {
2875        let mut registry = IncrementalCancellationRegistryV0::with_limit(2);
2876
2877        registry.cancel("n:1");
2878        registry.cancel("n:2");
2879        registry.cancel("n:3");
2880
2881        let snapshot = registry.snapshot();
2882        assert_eq!(snapshot.product, "omena-incremental.cancellation-registry");
2883        assert_eq!(snapshot.cancelled_request_ids, vec!["n:3"]);
2884    }
2885
2886    fn sample_input(a_digest: &str, b_digest: &str, revision: u64) -> IncrementalGraphInputV0 {
2887        IncrementalGraphInputV0 {
2888            revision: IncrementalRevisionV0 { value: revision },
2889            nodes: vec![
2890                IncrementalNodeInputV0 {
2891                    id: "b".to_string(),
2892                    digest: b_digest.to_string(),
2893                    dependency_ids: vec!["a".to_string()],
2894                },
2895                IncrementalNodeInputV0 {
2896                    id: "a".to_string(),
2897                    digest: a_digest.to_string(),
2898                    dependency_ids: Vec::new(),
2899                },
2900            ],
2901        }
2902    }
2903
2904    fn cyclic_input(a_digest: &str, b_digest: &str, revision: u64) -> IncrementalGraphInputV0 {
2905        IncrementalGraphInputV0 {
2906            revision: IncrementalRevisionV0 { value: revision },
2907            nodes: vec![
2908                IncrementalNodeInputV0 {
2909                    id: "a".to_string(),
2910                    digest: a_digest.to_string(),
2911                    dependency_ids: vec!["b".to_string()],
2912                },
2913                IncrementalNodeInputV0 {
2914                    id: "b".to_string(),
2915                    digest: b_digest.to_string(),
2916                    dependency_ids: vec!["a".to_string()],
2917                },
2918            ],
2919        }
2920    }
2921
2922    fn three_node_input(
2923        a_digest: &str,
2924        b_digest: &str,
2925        c_digest: &str,
2926        revision: u64,
2927    ) -> IncrementalGraphInputV0 {
2928        IncrementalGraphInputV0 {
2929            revision: IncrementalRevisionV0 { value: revision },
2930            nodes: vec![
2931                IncrementalNodeInputV0 {
2932                    id: "a".to_string(),
2933                    digest: a_digest.to_string(),
2934                    dependency_ids: Vec::new(),
2935                },
2936                IncrementalNodeInputV0 {
2937                    id: "b".to_string(),
2938                    digest: b_digest.to_string(),
2939                    dependency_ids: Vec::new(),
2940                },
2941                IncrementalNodeInputV0 {
2942                    id: "c".to_string(),
2943                    digest: c_digest.to_string(),
2944                    dependency_ids: vec!["a".to_string()],
2945                },
2946            ],
2947        }
2948    }
2949
2950    fn priority_input(
2951        node_id: &str,
2952        edit_distance_total: usize,
2953        cascade_margin_abs_distance: u64,
2954        bridge_checked: bool,
2955    ) -> super::IncrementalEditDistancePriorityInputV0 {
2956        super::IncrementalEditDistancePriorityInputV0 {
2957            schema_version: "0",
2958            product: "omena-incremental.edit-distance-priority-input",
2959            feature_gate: "incremental-edit-distance-priority-v0",
2960            claim_level: "fixtureWitnessMetricInput",
2961            theorem_claimed: false,
2962            node_id: node_id.to_string(),
2963            edit_distance_total,
2964            cascade_margin_abs_distance,
2965            bridge_checked,
2966            bridge_calibration_stage: "fixtureWitnessOnlyUncalibrated",
2967        }
2968    }
2969
2970    fn node_by_id<'a>(
2971        plan: &'a super::IncrementalComputationPlanV0,
2972        id: &str,
2973    ) -> Option<&'a super::IncrementalComputationNodeV0> {
2974        plan.nodes.iter().find(|node| node.id == id)
2975    }
2976
2977    fn node_reasons(plan: &super::IncrementalComputationPlanV0, id: &str) -> Vec<&'static str> {
2978        plan.nodes
2979            .iter()
2980            .find(|node| node.id == id)
2981            .map(|node| node.reasons.clone())
2982            .unwrap_or_default()
2983    }
2984
2985    fn plan_from_database(
2986        input: &IncrementalGraphInputV0,
2987        previous: Option<&super::IncrementalSnapshotV0>,
2988    ) -> super::IncrementalComputationPlanV0 {
2989        plan_from_database_with_priority_inputs(input, previous, &[])
2990    }
2991
2992    fn plan_from_database_with_priority_inputs(
2993        input: &IncrementalGraphInputV0,
2994        previous: Option<&super::IncrementalSnapshotV0>,
2995        priority_inputs: &[super::IncrementalEditDistancePriorityInputV0],
2996    ) -> super::IncrementalComputationPlanV0 {
2997        let mut database = OmenaIncrementalDatabaseV0::default();
2998        if let Some(previous) = previous {
2999            database.restore_snapshot(previous);
3000        }
3001        database
3002            .plan_and_upsert_graph_input_with_priority_inputs(input, priority_inputs)
3003            .incremental_plan
3004    }
3005}