Skip to main content

omena_parser/closed_world/
contract.rs

1//! Closed-world contract types: module identity, instance keys, linked-module
2//! facts, reachability, and the sealed bundle exposed to consumers.
3
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
9#[serde(rename_all = "camelCase")]
10pub struct ModuleIdV0(String);
11
12impl ModuleIdV0 {
13    pub fn new(value: impl Into<String>) -> Self {
14        Self(value.into())
15    }
16
17    pub fn as_str(&self) -> &str {
18        &self.0
19    }
20}
21
22impl From<&str> for ModuleIdV0 {
23    fn from(value: &str) -> Self {
24        Self::new(value)
25    }
26}
27
28impl From<String> for ModuleIdV0 {
29    fn from(value: String) -> Self {
30        Self::new(value)
31    }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ConfigurationHashV0(String);
37
38impl ConfigurationHashV0 {
39    pub fn new(value: impl Into<String>) -> Self {
40        Self(value.into())
41    }
42
43    pub fn none() -> Self {
44        Self::new("with:none")
45    }
46
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl Default for ConfigurationHashV0 {
53    fn default() -> Self {
54        Self::none()
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct ModuleInstanceKeyV0 {
61    module: ModuleIdV0,
62    configuration: ConfigurationHashV0,
63}
64
65impl ModuleInstanceKeyV0 {
66    pub fn new(module: ModuleIdV0, configuration: ConfigurationHashV0) -> Self {
67        Self {
68            module,
69            configuration,
70        }
71    }
72
73    pub fn unconfigured(module: ModuleIdV0) -> Self {
74        Self::new(module, ConfigurationHashV0::none())
75    }
76
77    pub fn module(&self) -> &ModuleIdV0 {
78        &self.module
79    }
80
81    pub fn configuration(&self) -> &ConfigurationHashV0 {
82        &self.configuration
83    }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct ClosedWorldComposesEdgeV0 {
89    pub from_module: ModuleInstanceKeyV0,
90    pub from_symbol: String,
91    pub to_module: ModuleInstanceKeyV0,
92    pub to_symbol: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct ClosedWorldLinkedModuleV0 {
98    pub instance: ModuleInstanceKeyV0,
99    pub dependencies: Vec<ModuleInstanceKeyV0>,
100    pub composes_edges: Vec<ClosedWorldComposesEdgeV0>,
101    pub composes_edge_observation_count: usize,
102    pub class_names: Vec<String>,
103    pub keyframe_names: Vec<String>,
104    pub value_names: Vec<String>,
105    pub custom_property_names: Vec<String>,
106}
107
108impl ClosedWorldLinkedModuleV0 {
109    pub fn new(instance: ModuleInstanceKeyV0) -> Self {
110        Self {
111            instance,
112            dependencies: Vec::new(),
113            composes_edges: Vec::new(),
114            composes_edge_observation_count: 0,
115            class_names: Vec::new(),
116            keyframe_names: Vec::new(),
117            value_names: Vec::new(),
118            custom_property_names: Vec::new(),
119        }
120    }
121
122    pub fn with_dependency(mut self, dependency: ModuleInstanceKeyV0) -> Self {
123        self.dependencies.push(dependency);
124        self
125    }
126
127    pub fn with_composes_edge(mut self, edge: ClosedWorldComposesEdgeV0) -> Self {
128        self.composes_edges.push(edge);
129        self.composes_edge_observation_count =
130            self.composes_edge_observation_count.saturating_add(1);
131        self
132    }
133
134    pub fn with_class_name(mut self, name: impl Into<String>) -> Self {
135        self.class_names.push(name.into());
136        self
137    }
138
139    pub fn with_keyframe_name(mut self, name: impl Into<String>) -> Self {
140        self.keyframe_names.push(name.into());
141        self
142    }
143
144    pub fn with_value_name(mut self, name: impl Into<String>) -> Self {
145        self.value_names.push(name.into());
146        self
147    }
148
149    pub fn with_custom_property_name(mut self, name: impl Into<String>) -> Self {
150        self.custom_property_names.push(name.into());
151        self
152    }
153}
154
155#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
156#[serde(rename_all = "camelCase")]
157pub struct ClosedWorldSourcePrecisionSummaryV0 {
158    pub exact_source_count: usize,
159    pub conservative_source_count: usize,
160    pub heuristic_source_count: usize,
161    pub unknown_source_count: usize,
162}
163
164impl ClosedWorldSourcePrecisionSummaryV0 {
165    pub(crate) fn merge(&mut self, other: Self) {
166        self.exact_source_count = self
167            .exact_source_count
168            .saturating_add(other.exact_source_count);
169        self.conservative_source_count = self
170            .conservative_source_count
171            .saturating_add(other.conservative_source_count);
172        self.heuristic_source_count = self
173            .heuristic_source_count
174            .saturating_add(other.heuristic_source_count);
175        self.unknown_source_count = self
176            .unknown_source_count
177            .saturating_add(other.unknown_source_count);
178    }
179}
180
181#[non_exhaustive]
182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub enum ClosedWorldModuleReachabilityEvidenceV0 {
185    Supplied,
186    #[default]
187    ModuleReachabilityInputAbsent,
188}
189
190#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
191#[serde(rename_all = "camelCase")]
192pub enum ClosedWorldComposesScanStateV0 {
193    ScannedClosed,
194    #[default]
195    SourceSetOpen,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct ClosedWorldModuleMetadataV0 {
200    module_instance: ModuleInstanceKeyV0,
201    interface_hash: Option<String>,
202    source_precision: Option<ClosedWorldSourcePrecisionSummaryV0>,
203    reachability_evidence: ClosedWorldModuleReachabilityEvidenceV0,
204    composes_scan_state: ClosedWorldComposesScanStateV0,
205}
206
207impl ClosedWorldModuleMetadataV0 {
208    pub fn new(module_instance: ModuleInstanceKeyV0) -> Self {
209        Self {
210            module_instance,
211            interface_hash: None,
212            source_precision: None,
213            reachability_evidence:
214                ClosedWorldModuleReachabilityEvidenceV0::ModuleReachabilityInputAbsent,
215            composes_scan_state: ClosedWorldComposesScanStateV0::SourceSetOpen,
216        }
217    }
218
219    pub fn module_instance(&self) -> &ModuleInstanceKeyV0 {
220        &self.module_instance
221    }
222
223    pub fn with_interface_hash(mut self, interface_hash: impl Into<String>) -> Self {
224        self.interface_hash = Some(interface_hash.into());
225        self
226    }
227
228    pub fn interface_hash(&self) -> Option<&str> {
229        self.interface_hash.as_deref()
230    }
231
232    pub fn with_source_precision(
233        mut self,
234        source_precision: ClosedWorldSourcePrecisionSummaryV0,
235    ) -> Self {
236        self.source_precision = Some(source_precision);
237        self
238    }
239
240    pub fn source_precision(&self) -> Option<ClosedWorldSourcePrecisionSummaryV0> {
241        self.source_precision
242    }
243
244    pub fn with_reachability_evidence(
245        mut self,
246        reachability_evidence: ClosedWorldModuleReachabilityEvidenceV0,
247    ) -> Self {
248        self.reachability_evidence = reachability_evidence;
249        self
250    }
251
252    pub fn reachability_evidence(&self) -> ClosedWorldModuleReachabilityEvidenceV0 {
253        self.reachability_evidence
254    }
255
256    pub fn with_composes_scan_state(
257        mut self,
258        composes_scan_state: ClosedWorldComposesScanStateV0,
259    ) -> Self {
260        self.composes_scan_state = composes_scan_state;
261        self
262    }
263
264    pub fn composes_scan_state(&self) -> ClosedWorldComposesScanStateV0 {
265        self.composes_scan_state
266    }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
270#[serde(rename_all = "camelCase", tag = "status")]
271pub enum ClosedWorldInterfaceHashAvailabilityV0 {
272    Known { interface_hash: String },
273    Absent,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
277#[serde(rename_all = "camelCase")]
278pub struct ClosedWorldInterfaceHashEntryV0 {
279    pub module_instance: ModuleInstanceKeyV0,
280    pub availability: ClosedWorldInterfaceHashAvailabilityV0,
281}
282
283#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
284#[serde(rename_all = "camelCase")]
285pub struct ClosedWorldInterfaceHashSetV0 {
286    entries: Vec<ClosedWorldInterfaceHashEntryV0>,
287}
288
289impl ClosedWorldInterfaceHashSetV0 {
290    pub(crate) fn new(entries: Vec<ClosedWorldInterfaceHashEntryV0>) -> Self {
291        Self { entries }
292    }
293
294    pub fn entries(&self) -> &[ClosedWorldInterfaceHashEntryV0] {
295        &self.entries
296    }
297
298    pub fn all_absent(&self) -> bool {
299        self.entries.iter().all(|entry| {
300            matches!(
301                entry.availability,
302                ClosedWorldInterfaceHashAvailabilityV0::Absent
303            )
304        })
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
309#[serde(rename_all = "camelCase")]
310pub struct ModuleQualifiedSymbolSetV0 {
311    module_instance: ModuleInstanceKeyV0,
312    reachable: bool,
313    class_names: Vec<String>,
314    keyframe_names: Vec<String>,
315    value_names: Vec<String>,
316    custom_property_names: Vec<String>,
317}
318
319impl ModuleQualifiedSymbolSetV0 {
320    pub(crate) fn new(
321        module_instance: ModuleInstanceKeyV0,
322        reachable: bool,
323        class_names: Vec<String>,
324        keyframe_names: Vec<String>,
325        value_names: Vec<String>,
326        custom_property_names: Vec<String>,
327    ) -> Self {
328        Self {
329            module_instance,
330            reachable,
331            class_names,
332            keyframe_names,
333            value_names,
334            custom_property_names,
335        }
336    }
337
338    pub fn module_instance(&self) -> &ModuleInstanceKeyV0 {
339        &self.module_instance
340    }
341
342    pub fn is_reachable(&self) -> bool {
343        self.reachable
344    }
345
346    pub fn class_names(&self) -> &[String] {
347        &self.class_names
348    }
349
350    pub fn keyframe_names(&self) -> &[String] {
351        &self.keyframe_names
352    }
353
354    pub fn value_names(&self) -> &[String] {
355        &self.value_names
356    }
357
358    pub fn custom_property_names(&self) -> &[String] {
359        &self.custom_property_names
360    }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
364#[serde(rename_all = "camelCase")]
365pub struct ReachabilityIndexV0 {
366    module_instances: Vec<ModuleInstanceKeyV0>,
367    module_qualified_symbols: Vec<ModuleQualifiedSymbolSetV0>,
368    class_names: Vec<String>,
369    keyframe_names: Vec<String>,
370    value_names: Vec<String>,
371    custom_property_names: Vec<String>,
372}
373
374impl ReachabilityIndexV0 {
375    pub(crate) fn from_parts(
376        module_instances: Vec<ModuleInstanceKeyV0>,
377        module_qualified_symbols: Vec<ModuleQualifiedSymbolSetV0>,
378        class_names: Vec<String>,
379        keyframe_names: Vec<String>,
380        value_names: Vec<String>,
381        custom_property_names: Vec<String>,
382    ) -> Self {
383        Self {
384            module_instances,
385            module_qualified_symbols,
386            class_names,
387            keyframe_names,
388            value_names,
389            custom_property_names,
390        }
391    }
392
393    pub fn module_instances(&self) -> &[ModuleInstanceKeyV0] {
394        &self.module_instances
395    }
396
397    pub fn module_qualified_symbols(&self) -> &[ModuleQualifiedSymbolSetV0] {
398        &self.module_qualified_symbols
399    }
400
401    pub fn symbols_for_module(
402        &self,
403        module_instance: &ModuleInstanceKeyV0,
404    ) -> Option<&ModuleQualifiedSymbolSetV0> {
405        self.module_qualified_symbols
406            .binary_search_by(|entry| entry.module_instance().cmp(module_instance))
407            .ok()
408            .map(|index| &self.module_qualified_symbols[index])
409    }
410
411    pub fn class_names(&self) -> &[String] {
412        &self.class_names
413    }
414
415    pub fn keyframe_names(&self) -> &[String] {
416        &self.keyframe_names
417    }
418
419    pub fn value_names(&self) -> &[String] {
420        &self.value_names
421    }
422
423    pub fn custom_property_names(&self) -> &[String] {
424        &self.custom_property_names
425    }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
429#[serde(rename_all = "camelCase")]
430pub struct ClosedWorldReachabilityBitsetParityReportV0 {
431    pub schema_version: &'static str,
432    pub product: &'static str,
433    pub module_instance_count: usize,
434    pub symbol_name_count: usize,
435    pub module_qualified_symbols: Vec<ModuleQualifiedSymbolSetV0>,
436    pub reachability_equal: bool,
437    pub closure_hash_equal: bool,
438    pub btreeset_closure_hash: String,
439    pub bitset_closure_hash: String,
440}
441
442/// Closed-world bundle constructed from linked module facts.
443///
444/// External callers cannot use field-literal construction:
445///
446/// ```compile_fail
447/// use omena_parser::ClosedWorldBundleV0;
448///
449/// let _bundle = ClosedWorldBundleV0 {
450///     closure_hash: String::new(),
451/// };
452/// ```
453#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
454#[serde(rename_all = "camelCase")]
455pub struct ClosedWorldBundleV0 {
456    entrypoints: Vec<ModuleInstanceKeyV0>,
457    linked_modules: Vec<ModuleInstanceKeyV0>,
458    reachability: ReachabilityIndexV0,
459    closure_hash: String,
460    #[serde(skip_serializing_if = "ClosedWorldInterfaceHashSetV0::all_absent")]
461    interface_hashes: ClosedWorldInterfaceHashSetV0,
462    #[serde(skip_serializing_if = "Option::is_none")]
463    source_precision: Option<ClosedWorldSourcePrecisionSummaryV0>,
464    #[serde(skip_serializing_if = "Vec::is_empty")]
465    composes_edges: Vec<ClosedWorldComposesEdgeV0>,
466    #[serde(skip)]
467    composes_scan_state: ClosedWorldComposesScanStateV0,
468    #[serde(skip)]
469    composes_edge_observation_count: usize,
470    #[serde(skip)]
471    composes_origin_class_names: BTreeMap<ModuleInstanceKeyV0, Vec<String>>,
472    #[serde(skip)]
473    module_reachability_evidence:
474        BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
475}
476
477pub(super) struct ClosedWorldBundleEvidenceV0 {
478    pub(super) interface_hashes: ClosedWorldInterfaceHashSetV0,
479    pub(super) source_precision: Option<ClosedWorldSourcePrecisionSummaryV0>,
480    pub(super) composes_edges: Vec<ClosedWorldComposesEdgeV0>,
481    pub(super) composes_scan_state: ClosedWorldComposesScanStateV0,
482    pub(super) composes_edge_observation_count: usize,
483    pub(super) composes_origin_class_names: BTreeMap<ModuleInstanceKeyV0, Vec<String>>,
484    pub(super) module_reachability_evidence:
485        BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
486}
487
488impl ClosedWorldBundleV0 {
489    pub(super) fn seal(
490        entrypoints: Vec<ModuleInstanceKeyV0>,
491        linked_modules: Vec<ModuleInstanceKeyV0>,
492        reachability: ReachabilityIndexV0,
493        closure_hash: String,
494        evidence: ClosedWorldBundleEvidenceV0,
495    ) -> Self {
496        let ClosedWorldBundleEvidenceV0 {
497            interface_hashes,
498            source_precision,
499            composes_edges,
500            composes_scan_state,
501            composes_edge_observation_count,
502            composes_origin_class_names,
503            module_reachability_evidence,
504        } = evidence;
505        Self {
506            entrypoints,
507            linked_modules,
508            reachability,
509            closure_hash,
510            interface_hashes,
511            source_precision,
512            composes_edges,
513            composes_scan_state,
514            composes_edge_observation_count,
515            composes_origin_class_names,
516            module_reachability_evidence,
517        }
518    }
519
520    pub fn entrypoints(&self) -> &[ModuleInstanceKeyV0] {
521        &self.entrypoints
522    }
523
524    pub fn linked_modules(&self) -> &[ModuleInstanceKeyV0] {
525        &self.linked_modules
526    }
527
528    pub fn reachability(&self) -> &ReachabilityIndexV0 {
529        &self.reachability
530    }
531
532    pub fn closure_hash(&self) -> &str {
533        &self.closure_hash
534    }
535
536    pub fn module_qualified_ownership_digest(&self) -> String {
537        let mut digest = StableModuleOwnershipDigestV0::new();
538        let mut modules = self
539            .reachability
540            .module_qualified_symbols()
541            .iter()
542            .filter(|symbols| symbols.is_reachable())
543            .collect::<Vec<_>>();
544        modules.sort_by(|left, right| left.module_instance().cmp(right.module_instance()));
545
546        for symbols in modules {
547            digest.module(symbols.module_instance());
548            digest.symbol_names("class", symbols.class_names());
549            digest.symbol_names("keyframe", symbols.keyframe_names());
550            digest.symbol_names("value", symbols.value_names());
551            digest.symbol_names("custom-property", symbols.custom_property_names());
552        }
553
554        digest.finish_hex()
555    }
556
557    pub fn interface_hashes(&self) -> &ClosedWorldInterfaceHashSetV0 {
558        &self.interface_hashes
559    }
560
561    pub fn source_precision(&self) -> Option<ClosedWorldSourcePrecisionSummaryV0> {
562        self.source_precision
563    }
564
565    pub fn composes_edges(&self) -> &[ClosedWorldComposesEdgeV0] {
566        self.composes_edges.as_slice()
567    }
568
569    pub fn composes_scan_state(&self) -> ClosedWorldComposesScanStateV0 {
570        self.composes_scan_state
571    }
572
573    pub fn composes_edge_observation_count(&self) -> usize {
574        self.composes_edge_observation_count
575    }
576
577    pub fn composes_origin_symbol_is_reachable(
578        &self,
579        module_instance: &ModuleInstanceKeyV0,
580        symbol: &str,
581    ) -> Option<bool> {
582        match self
583            .module_reachability_evidence
584            .get(module_instance)
585            .copied()
586        {
587            Some(ClosedWorldModuleReachabilityEvidenceV0::Supplied) => Some(
588                self.composes_origin_class_names
589                    .get(module_instance)
590                    .is_some_and(|class_names| class_names.iter().any(|name| name == symbol)),
591            ),
592            Some(ClosedWorldModuleReachabilityEvidenceV0::ModuleReachabilityInputAbsent) | None => {
593                None
594            }
595        }
596    }
597
598    pub fn module_reachability_evidence(
599        &self,
600        module_instance: &ModuleInstanceKeyV0,
601    ) -> ClosedWorldModuleReachabilityEvidenceV0 {
602        self.module_reachability_evidence
603            .get(module_instance)
604            .copied()
605            .unwrap_or_default()
606    }
607}
608
609struct StableModuleOwnershipDigestV0(u64);
610
611impl StableModuleOwnershipDigestV0 {
612    fn new() -> Self {
613        let mut digest = Self(0xcbf2_9ce4_8422_2325);
614        digest.piece("omena-parser.module-qualified-ownership");
615        digest
616    }
617
618    fn piece(&mut self, value: &str) {
619        for byte in value.as_bytes().iter().copied().chain([0]) {
620            self.0 ^= u64::from(byte);
621            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
622        }
623    }
624
625    fn module(&mut self, module_instance: &ModuleInstanceKeyV0) {
626        self.piece("module");
627        self.piece(module_instance.module().as_str());
628        self.piece(module_instance.configuration().as_str());
629    }
630
631    fn symbol_names(&mut self, kind: &str, names: &[String]) {
632        self.piece(kind);
633        let names = names
634            .iter()
635            .map(String::as_str)
636            .collect::<std::collections::BTreeSet<_>>();
637        for name in names {
638            self.piece(name);
639        }
640    }
641
642    fn finish_hex(self) -> String {
643        format!("{:016x}", self.0)
644    }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
648#[serde(rename_all = "camelCase")]
649pub struct OpenWorldSnapshotV0 {
650    reason: String,
651}
652
653impl OpenWorldSnapshotV0 {
654    pub fn new(reason: impl Into<String>) -> Self {
655        Self {
656            reason: reason.into(),
657        }
658    }
659
660    pub fn reason(&self) -> &str {
661        &self.reason
662    }
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
666#[serde(rename_all = "camelCase")]
667pub enum ClosedWorldBundleBuildErrorV0 {
668    EmptyEntrypoints,
669    MissingEntrypoint {
670        module: ModuleInstanceKeyV0,
671    },
672    MissingDependency {
673        module: ModuleInstanceKeyV0,
674        dependency: ModuleInstanceKeyV0,
675    },
676}