1use 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")]
284pub 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}