Skip to main content

xlog_logic/resolver/
manifest.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::{Component, Path, PathBuf};
4
5use serde::Serialize;
6use sha2::{Digest, Sha256};
7
8use crate::ast::Program;
9use crate::incremental_parse::{ParserSession, StatementSpan};
10use crate::module::LoadedModule;
11use crate::parser::parse_program;
12
13use super::ModuleResolver;
14
15const MANIFEST_SCHEMA_VERSION: &str = "xlog.resolved-program-manifest.v1";
16const MODULE_ID_DOMAIN: &[u8] = b"xlog.resolved-module.v1\0";
17
18/// A deterministic, content-addressed inventory of one resolved XLOG program.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20pub struct ResolvedProgramManifest {
21    /// Version of the machine-readable manifest contract.
22    pub schema_version: String,
23    /// Content-addressed identifier of the compilation entry module.
24    pub entry_module_id: String,
25    /// Complete entrypoint and transitive-import closure, sorted by source path.
26    pub modules: Vec<ResolvedModuleManifest>,
27    /// Import edges in deterministic importer and authored declaration order.
28    pub imports: Vec<ResolvedImportManifest>,
29    /// Complete source-object counts, sorted by construct kind.
30    pub construct_inventory: Vec<ResolvedConstructCount>,
31}
32
33/// One source file in a resolved program closure.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct ResolvedModuleManifest {
36    /// Identifier derived from the source-root-relative path and exact bytes.
37    pub module_id: String,
38    /// UTF-8 source-root-relative path with `/` separators.
39    pub source_path: String,
40    /// SHA-256 of the exact UTF-8 bytes parsed by the resolver.
41    pub content_sha256: String,
42    /// Logical paths that reach this source from the current entry closure.
43    pub logical_paths: Vec<String>,
44    /// Source objects in authored statement order.
45    pub source_objects: Vec<ResolvedSourceObject>,
46}
47
48/// One resolved import edge.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50pub struct ResolvedImportManifest {
51    /// Module containing the authored `use` declaration.
52    pub importer_module_id: String,
53    /// Module selected by importer-scoped resolution.
54    pub target_module_id: String,
55    /// Zero-based authored import ordinal in the importer.
56    pub authored_ordinal: usize,
57    /// Source object that owns this edge.
58    pub source_object_id: String,
59    /// Path as written by the importer.
60    pub declared_path: Vec<String>,
61    /// Contextual logical path selected by the resolver.
62    pub resolved_path: Vec<String>,
63    /// Explicitly selected public items, or `None` for the complete public surface.
64    pub imported_items: Option<Vec<String>>,
65}
66
67/// One parsed source object and its exact authored source location.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
69pub struct ResolvedSourceObject {
70    /// Stable identity within the content-addressed module.
71    pub object_id: String,
72    /// Parsed construct category.
73    pub kind: ResolvedSourceObjectKind,
74    /// Whether the object was authored or generated by a compiler transform.
75    pub provenance: ResolvedSourceObjectProvenance,
76    /// Zero-based order among all objects in the module.
77    pub authored_ordinal: usize,
78    /// Zero-based order among objects of the same kind in the module.
79    pub kind_ordinal: usize,
80    /// Zero-based order of the source statement containing this object.
81    pub statement_ordinal: usize,
82    /// SHA-256 of the exact statement bytes.
83    pub content_sha256: String,
84    /// Parser-owned byte and line/column span.
85    pub span: ResolvedSourceSpan,
86    /// Primary declared or head name when the construct has one.
87    pub primary_name: Option<String>,
88    /// Declared or head arity when the construct has one.
89    pub arity: Option<usize>,
90}
91
92/// Origin of a source object exposed by the resolved-program inventory.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ResolvedSourceObjectProvenance {
96    /// The object came directly from a parser-owned source statement.
97    Authored,
98}
99
100/// Parser-owned source span copied into the stable manifest contract.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
102pub struct ResolvedSourceSpan {
103    /// Zero-based byte offset where the statement starts.
104    pub start: usize,
105    /// Exclusive zero-based byte offset where the statement ends.
106    pub end: usize,
107    /// One-based line where the statement starts.
108    pub line: usize,
109    /// One-based column where the statement starts.
110    pub column: usize,
111}
112
113impl From<StatementSpan> for ResolvedSourceSpan {
114    fn from(span: StatementSpan) -> Self {
115        Self {
116            start: span.start,
117            end: span.end,
118            line: span.line,
119            column: span.column,
120        }
121    }
122}
123
124/// Source-level constructs represented by the canonical parser.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
126#[serde(rename_all = "snake_case")]
127pub enum ResolvedSourceObjectKind {
128    /// A `use` declaration.
129    Import,
130    /// A user-defined function declaration.
131    Function,
132    /// A scalar domain alias declaration.
133    Domain,
134    /// A predicate schema declaration.
135    Predicate,
136    /// A fact or rule clause.
137    Rule,
138    /// An integrity constraint.
139    Constraint,
140    /// A deterministic query.
141    Query,
142    /// A probabilistic fact.
143    ProbabilisticFact,
144    /// An annotated disjunction.
145    AnnotatedDisjunction,
146    /// A probabilistic evidence statement.
147    Evidence,
148    /// A probabilistic query.
149    ProbabilisticQuery,
150    /// A neural predicate declaration.
151    NeuralPredicate,
152    /// A learnable rule template.
153    LearnableRule,
154    /// A compilation directive.
155    Directive,
156}
157
158impl ResolvedSourceObjectKind {
159    fn as_str(self) -> &'static str {
160        match self {
161            Self::Import => "import",
162            Self::Function => "function",
163            Self::Domain => "domain",
164            Self::Predicate => "predicate",
165            Self::Rule => "rule",
166            Self::Constraint => "constraint",
167            Self::Query => "query",
168            Self::ProbabilisticFact => "probabilistic_fact",
169            Self::AnnotatedDisjunction => "annotated_disjunction",
170            Self::Evidence => "evidence",
171            Self::ProbabilisticQuery => "probabilistic_query",
172            Self::NeuralPredicate => "neural_predicate",
173            Self::LearnableRule => "learnable_rule",
174            Self::Directive => "directive",
175        }
176    }
177}
178
179/// Total number of source objects of one construct kind.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
181pub struct ResolvedConstructCount {
182    /// Counted source-object kind.
183    pub kind: ResolvedSourceObjectKind,
184    /// Number of objects of this kind in the complete closure.
185    pub count: usize,
186}
187
188/// Fail-closed errors produced while binding a resolved closure to a source root.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum ResolvedProgramManifestError {
191    /// The resolver was not loaded from an exact entry file.
192    MissingEntry,
193    /// The resolved import surface is not valid under execution-time merge rules.
194    ModuleValidation {
195        /// Canonical module-resolution diagnostic.
196        message: String,
197    },
198    /// The declared source root is missing, inaccessible, or not a directory.
199    SourceRoot {
200        /// Supplied source-root path.
201        path: PathBuf,
202        /// Filesystem error detail.
203        message: String,
204    },
205    /// A resolved source escapes the declared source root.
206    SourceOutsideRoot {
207        /// Canonical source path outside the root.
208        path: PathBuf,
209        /// Canonical declared source root.
210        source_root: PathBuf,
211    },
212    /// A relative source path cannot be represented in the JSON contract.
213    NonUtf8Path {
214        /// Source path containing non-UTF-8 components.
215        path: PathBuf,
216    },
217    /// The resolver lacks the exact source text used for a loaded AST.
218    MissingSourceText {
219        /// Canonical source path whose parsed text is absent.
220        path: PathBuf,
221    },
222    /// A module in the current closure has no current-entry logical path.
223    MissingLogicalPath {
224        /// Canonical source path lacking a current logical path.
225        path: PathBuf,
226    },
227    /// A parser-owned statement could not be classified by the canonical parser.
228    StatementParse {
229        /// Source-root-relative module path.
230        source_path: String,
231        /// Zero-based authored statement ordinal.
232        statement_ordinal: usize,
233        /// Canonical parser diagnostic.
234        message: String,
235    },
236    /// A parser-owned statement span does not address bytes in the retained source.
237    InvalidStatementSpan {
238        /// Source-root-relative module path.
239        source_path: String,
240        /// Zero-based authored statement ordinal.
241        statement_ordinal: usize,
242        /// Reported inclusive start byte.
243        start: usize,
244        /// Reported exclusive end byte.
245        end: usize,
246        /// Length of the retained source in bytes.
247        source_len: usize,
248    },
249    /// A parsed statement produced no source-level AST object.
250    StatementWithoutObject {
251        /// Source-root-relative module path.
252        source_path: String,
253        /// Zero-based authored statement ordinal.
254        statement_ordinal: usize,
255    },
256    /// Authored imports, resolved edges, and import source objects disagree.
257    ImportGraphMismatch {
258        /// Source-root-relative importer path.
259        source_path: String,
260        /// Number of authored import declarations.
261        declarations: usize,
262        /// Number of importer-scoped resolved edges.
263        resolved_edges: usize,
264        /// Number of import source objects.
265        import_objects: usize,
266    },
267    /// A resolved edge points outside the loaded reachable closure.
268    MissingImportTarget {
269        /// Canonical target path absent from the closure.
270        path: PathBuf,
271    },
272}
273
274impl fmt::Display for ResolvedProgramManifestError {
275    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
276        match self {
277            Self::MissingEntry => write!(
278                formatter,
279                "resolved program manifest requires an entry file"
280            ),
281            Self::ModuleValidation { message } => {
282                write!(formatter, "resolved program validation failed: {message}")
283            }
284            Self::SourceRoot { path, message } => write!(
285                formatter,
286                "invalid source root '{}': {message}",
287                path.display()
288            ),
289            Self::SourceOutsideRoot { path, source_root } => write!(
290                formatter,
291                "resolved source '{}' is outside source root '{}'",
292                path.display(),
293                source_root.display()
294            ),
295            Self::NonUtf8Path { path } => {
296                write!(
297                    formatter,
298                    "resolved source path is not UTF-8: '{}'",
299                    path.display()
300                )
301            }
302            Self::MissingSourceText { path } => write!(
303                formatter,
304                "resolver did not retain parsed source bytes for '{}'",
305                path.display()
306            ),
307            Self::MissingLogicalPath { path } => write!(
308                formatter,
309                "resolved source has no logical path in the current entry closure: '{}'",
310                path.display()
311            ),
312            Self::StatementParse {
313                source_path,
314                statement_ordinal,
315                message,
316            } => write!(
317                formatter,
318                "failed to classify statement {statement_ordinal} in '{source_path}': {message}"
319            ),
320            Self::InvalidStatementSpan {
321                source_path,
322                statement_ordinal,
323                start,
324                end,
325                source_len,
326            } => write!(
327                formatter,
328                "invalid byte span {start}..{end} for statement {statement_ordinal} in \
329                 '{source_path}' (source length {source_len})"
330            ),
331            Self::StatementWithoutObject {
332                source_path,
333                statement_ordinal,
334            } => write!(
335                formatter,
336                "statement {statement_ordinal} in '{source_path}' produced no source object"
337            ),
338            Self::ImportGraphMismatch {
339                source_path,
340                declarations,
341                resolved_edges,
342                import_objects,
343            } => write!(
344                formatter,
345                "import graph mismatch in '{source_path}': {declarations} declarations, \
346                 {resolved_edges} resolved edges, {import_objects} import objects"
347            ),
348            Self::MissingImportTarget { path } => write!(
349                formatter,
350                "resolved import target is absent from the manifest closure: '{}'",
351                path.display()
352            ),
353        }
354    }
355}
356
357impl std::error::Error for ResolvedProgramManifestError {}
358
359#[derive(Clone)]
360struct ModuleIdentity {
361    module_id: String,
362    source_path: String,
363    content_sha256: String,
364}
365
366struct SourceModule<'a> {
367    source: &'a PathBuf,
368    module: &'a LoadedModule,
369    source_text: &'a str,
370    is_entry: bool,
371}
372
373#[derive(Debug)]
374struct ObjectDescriptor {
375    kind: ResolvedSourceObjectKind,
376    primary_name: Option<String>,
377    arity: Option<usize>,
378}
379
380impl ModuleResolver {
381    /// Build a deterministic source inventory from the exact bytes parsed by this resolver.
382    ///
383    /// Every source must resolve beneath `source_root`. Paths in the result are relative
384    /// and platform-independent, while module and statement identities bind those paths
385    /// to SHA-256 digests of the parsed bytes.
386    pub fn resolved_program_manifest(
387        &self,
388        source_root: &Path,
389    ) -> Result<ResolvedProgramManifest, ResolvedProgramManifestError> {
390        let source_root = std::fs::canonicalize(source_root).map_err(|error| {
391            ResolvedProgramManifestError::SourceRoot {
392                path: source_root.to_path_buf(),
393                message: error.to_string(),
394            }
395        })?;
396        if !source_root.is_dir() {
397            return Err(ResolvedProgramManifestError::SourceRoot {
398                path: source_root,
399                message: "path is not a directory".to_string(),
400            });
401        }
402        let entry = self
403            .entry
404            .as_ref()
405            .ok_or(ResolvedProgramManifestError::MissingEntry)?;
406        let entry_source = self
407            .entry_source
408            .as_ref()
409            .ok_or(ResolvedProgramManifestError::MissingEntry)?;
410        let entry_source_text = self.entry_source_text.as_deref().ok_or_else(|| {
411            ResolvedProgramManifestError::MissingSourceText {
412                path: entry_source.clone(),
413            }
414        })?;
415        self.merge_imports(entry.program.clone()).map_err(|error| {
416            ResolvedProgramManifestError::ModuleValidation {
417                message: error.to_string(),
418            }
419        })?;
420
421        let mut reachable_sources = BTreeSet::new();
422        let mut pending_sources = self
423            .entry_resolved_imports
424            .iter()
425            .map(|edge| edge.source.clone())
426            .collect::<Vec<_>>();
427        while let Some(source) = pending_sources.pop() {
428            if !reachable_sources.insert(source.clone()) {
429                continue;
430            }
431            if let Some(edges) = self.resolved_imports.get(&source) {
432                pending_sources.extend(edges.iter().map(|edge| edge.source.clone()));
433            }
434        }
435
436        let mut source_modules = vec![SourceModule {
437            source: entry_source,
438            module: entry,
439            source_text: entry_source_text,
440            is_entry: true,
441        }];
442        for source in &reachable_sources {
443            let module = self.loaded.get(source).ok_or_else(|| {
444                ResolvedProgramManifestError::MissingImportTarget {
445                    path: source.clone(),
446                }
447            })?;
448            let source_text = self.loaded_source_texts.get(source).ok_or_else(|| {
449                ResolvedProgramManifestError::MissingSourceText {
450                    path: source.clone(),
451                }
452            })?;
453            source_modules.push(SourceModule {
454                source,
455                module,
456                source_text,
457                is_entry: false,
458            });
459        }
460
461        let mut identities = BTreeMap::new();
462        for source_module in &source_modules {
463            let source_path = relative_source_path(&source_root, source_module.source)?;
464            let content_sha256 = sha256_prefixed(source_module.source_text.as_bytes());
465            let module_id = module_id(&source_path, &content_sha256);
466            identities.insert(
467                source_module.source.clone(),
468                ModuleIdentity {
469                    module_id,
470                    source_path,
471                    content_sha256,
472                },
473            );
474        }
475        source_modules.sort_by(|left, right| {
476            identities[left.source]
477                .source_path
478                .cmp(&identities[right.source].source_path)
479        });
480
481        let mut logical_paths_by_source = BTreeMap::<PathBuf, BTreeSet<String>>::new();
482        logical_paths_by_source
483            .entry(entry_source.clone())
484            .or_default()
485            .insert(entry.path.join("/"));
486        for edge in &self.entry_resolved_imports {
487            logical_paths_by_source
488                .entry(edge.source.clone())
489                .or_default()
490                .insert(edge.module_path.join("/"));
491        }
492        for source in &reachable_sources {
493            if let Some(edges) = self.resolved_imports.get(source) {
494                for edge in edges {
495                    logical_paths_by_source
496                        .entry(edge.source.clone())
497                        .or_default()
498                        .insert(edge.module_path.join("/"));
499                }
500            }
501        }
502
503        let entry_module_id = identities[entry_source].module_id.clone();
504        let mut modules = Vec::with_capacity(source_modules.len());
505        for source_module in &source_modules {
506            let identity = &identities[source_module.source];
507            let logical_paths = logical_paths_by_source
508                .get(source_module.source)
509                .ok_or_else(|| ResolvedProgramManifestError::MissingLogicalPath {
510                    path: source_module.source.clone(),
511                })?;
512            modules.push(ResolvedModuleManifest {
513                module_id: identity.module_id.clone(),
514                source_path: identity.source_path.clone(),
515                content_sha256: identity.content_sha256.clone(),
516                logical_paths: logical_paths.iter().cloned().collect(),
517                source_objects: build_source_objects(
518                    &identity.module_id,
519                    &identity.source_path,
520                    source_module.source_text,
521                )?,
522            });
523        }
524
525        let module_by_id = modules
526            .iter()
527            .map(|module| (module.module_id.clone(), module))
528            .collect::<BTreeMap<_, _>>();
529        let mut imports = Vec::new();
530        for source_module in &source_modules {
531            let identity = &identities[source_module.source];
532            let resolved_edges = if source_module.is_entry {
533                self.entry_resolved_imports.as_slice()
534            } else {
535                self.resolved_imports
536                    .get(source_module.source)
537                    .map(Vec::as_slice)
538                    .unwrap_or_default()
539            };
540            let import_objects = module_by_id[&identity.module_id]
541                .source_objects
542                .iter()
543                .filter(|object| object.kind == ResolvedSourceObjectKind::Import)
544                .collect::<Vec<_>>();
545            if source_module.module.program.imports.len() != resolved_edges.len()
546                || resolved_edges.len() != import_objects.len()
547            {
548                return Err(ResolvedProgramManifestError::ImportGraphMismatch {
549                    source_path: identity.source_path.clone(),
550                    declarations: source_module.module.program.imports.len(),
551                    resolved_edges: resolved_edges.len(),
552                    import_objects: import_objects.len(),
553                });
554            }
555            for (authored_ordinal, ((declaration, edge), source_object)) in source_module
556                .module
557                .program
558                .imports
559                .iter()
560                .zip(resolved_edges)
561                .zip(import_objects)
562                .enumerate()
563            {
564                let target = identities.get(&edge.source).ok_or_else(|| {
565                    ResolvedProgramManifestError::MissingImportTarget {
566                        path: edge.source.clone(),
567                    }
568                })?;
569                imports.push(ResolvedImportManifest {
570                    importer_module_id: identity.module_id.clone(),
571                    target_module_id: target.module_id.clone(),
572                    authored_ordinal,
573                    source_object_id: source_object.object_id.clone(),
574                    declared_path: declaration.module_path.clone(),
575                    resolved_path: edge.module_path.clone(),
576                    imported_items: declaration.imports.clone(),
577                });
578            }
579        }
580
581        let mut inventory = BTreeMap::new();
582        for object in modules.iter().flat_map(|module| &module.source_objects) {
583            *inventory.entry(object.kind).or_insert(0usize) += 1;
584        }
585
586        Ok(ResolvedProgramManifest {
587            schema_version: MANIFEST_SCHEMA_VERSION.to_string(),
588            entry_module_id,
589            modules,
590            imports,
591            construct_inventory: inventory
592                .into_iter()
593                .map(|(kind, count)| ResolvedConstructCount { kind, count })
594                .collect(),
595        })
596    }
597}
598
599fn relative_source_path(
600    source_root: &Path,
601    source: &Path,
602) -> Result<String, ResolvedProgramManifestError> {
603    let relative = source.strip_prefix(source_root).map_err(|_| {
604        ResolvedProgramManifestError::SourceOutsideRoot {
605            path: source.to_path_buf(),
606            source_root: source_root.to_path_buf(),
607        }
608    })?;
609    let mut parts = Vec::new();
610    for component in relative.components() {
611        match component {
612            Component::Normal(part) => parts.push(
613                part.to_str()
614                    .ok_or_else(|| ResolvedProgramManifestError::NonUtf8Path {
615                        path: source.to_path_buf(),
616                    })?
617                    .to_string(),
618            ),
619            Component::CurDir => {}
620            _ => {
621                return Err(ResolvedProgramManifestError::SourceOutsideRoot {
622                    path: source.to_path_buf(),
623                    source_root: source_root.to_path_buf(),
624                });
625            }
626        }
627    }
628    if parts.is_empty() {
629        return Err(ResolvedProgramManifestError::NonUtf8Path {
630            path: source.to_path_buf(),
631        });
632    }
633    Ok(parts.join("/"))
634}
635
636fn module_id(source_path: &str, content_sha256: &str) -> String {
637    let mut digest = Sha256::new();
638    digest.update(MODULE_ID_DOMAIN);
639    digest.update((source_path.len() as u64).to_le_bytes());
640    digest.update(source_path.as_bytes());
641    digest.update((content_sha256.len() as u64).to_le_bytes());
642    digest.update(content_sha256.as_bytes());
643    format!("sha256:{}", hex_digest(digest.finalize()))
644}
645
646fn build_source_objects(
647    module_id: &str,
648    source_path: &str,
649    source_text: &str,
650) -> Result<Vec<ResolvedSourceObject>, ResolvedProgramManifestError> {
651    let mut objects = Vec::new();
652    let mut kind_ordinals = BTreeMap::new();
653    for (statement_ordinal, statement) in ParserSession::split_statements(source_text)
654        .into_iter()
655        .enumerate()
656    {
657        let parsed = parse_program(&statement.text).map_err(|error| {
658            ResolvedProgramManifestError::StatementParse {
659                source_path: source_path.to_string(),
660                statement_ordinal,
661                message: error.to_string(),
662            }
663        })?;
664        let descriptors = describe_objects(&parsed);
665        if descriptors.is_empty() {
666            return Err(ResolvedProgramManifestError::StatementWithoutObject {
667                source_path: source_path.to_string(),
668                statement_ordinal,
669            });
670        }
671        let span = statement.span;
672        let source_bytes = source_text.as_bytes();
673        if span.start > span.end || span.end > source_bytes.len() {
674            return Err(ResolvedProgramManifestError::InvalidStatementSpan {
675                source_path: source_path.to_string(),
676                statement_ordinal,
677                start: span.start,
678                end: span.end,
679                source_len: source_bytes.len(),
680            });
681        }
682        let content_sha256 = sha256_prefixed(&source_bytes[span.start..span.end]);
683        for descriptor in descriptors {
684            let kind_ordinal = kind_ordinals.entry(descriptor.kind).or_insert(0usize);
685            let object_id = format!("{module_id}#{}:{}", descriptor.kind.as_str(), *kind_ordinal);
686            objects.push(ResolvedSourceObject {
687                object_id,
688                kind: descriptor.kind,
689                provenance: ResolvedSourceObjectProvenance::Authored,
690                authored_ordinal: objects.len(),
691                kind_ordinal: *kind_ordinal,
692                statement_ordinal,
693                content_sha256: content_sha256.clone(),
694                span: span.into(),
695                primary_name: descriptor.primary_name,
696                arity: descriptor.arity,
697            });
698            *kind_ordinal += 1;
699        }
700    }
701    Ok(objects)
702}
703
704fn describe_objects(program: &Program) -> Vec<ObjectDescriptor> {
705    let mut objects = Vec::new();
706    objects.extend(program.imports.iter().map(|declaration| ObjectDescriptor {
707        kind: ResolvedSourceObjectKind::Import,
708        primary_name: Some(declaration.module_path.join("/")),
709        arity: None,
710    }));
711    objects.extend(program.functions.iter().map(|definition| ObjectDescriptor {
712        kind: ResolvedSourceObjectKind::Function,
713        primary_name: Some(definition.name.clone()),
714        arity: Some(definition.params.len()),
715    }));
716    objects.extend(program.domains.iter().map(|declaration| ObjectDescriptor {
717        kind: ResolvedSourceObjectKind::Domain,
718        primary_name: Some(declaration.name.clone()),
719        arity: None,
720    }));
721    objects.extend(
722        program
723            .predicates
724            .iter()
725            .map(|declaration| ObjectDescriptor {
726                kind: ResolvedSourceObjectKind::Predicate,
727                primary_name: Some(declaration.name.clone()),
728                arity: Some(declaration.schema_columns().len()),
729            }),
730    );
731    objects.extend(program.rules.iter().map(|rule| ObjectDescriptor {
732        kind: ResolvedSourceObjectKind::Rule,
733        primary_name: Some(rule.head.predicate.clone()),
734        arity: Some(rule.head.terms.len()),
735    }));
736    objects.extend(program.constraints.iter().map(|_| ObjectDescriptor {
737        kind: ResolvedSourceObjectKind::Constraint,
738        primary_name: None,
739        arity: None,
740    }));
741    objects.extend(program.queries.iter().map(|query| ObjectDescriptor {
742        kind: ResolvedSourceObjectKind::Query,
743        primary_name: Some(query.atom.predicate.clone()),
744        arity: Some(query.atom.terms.len()),
745    }));
746    objects.extend(program.prob_facts.iter().map(|fact| ObjectDescriptor {
747        kind: ResolvedSourceObjectKind::ProbabilisticFact,
748        primary_name: Some(fact.atom.predicate.clone()),
749        arity: Some(fact.atom.terms.len()),
750    }));
751    objects.extend(
752        program
753            .annotated_disjunctions
754            .iter()
755            .map(|_| ObjectDescriptor {
756                kind: ResolvedSourceObjectKind::AnnotatedDisjunction,
757                primary_name: None,
758                arity: None,
759            }),
760    );
761    objects.extend(program.evidence.iter().map(|evidence| ObjectDescriptor {
762        kind: ResolvedSourceObjectKind::Evidence,
763        primary_name: Some(evidence.atom.predicate.clone()),
764        arity: Some(evidence.atom.terms.len()),
765    }));
766    objects.extend(program.prob_queries.iter().map(|query| ObjectDescriptor {
767        kind: ResolvedSourceObjectKind::ProbabilisticQuery,
768        primary_name: Some(query.atom.predicate.clone()),
769        arity: Some(query.atom.terms.len()),
770    }));
771    objects.extend(
772        program
773            .neural_predicates
774            .iter()
775            .map(|declaration| ObjectDescriptor {
776                kind: ResolvedSourceObjectKind::NeuralPredicate,
777                primary_name: Some(declaration.predicate.predicate.clone()),
778                arity: Some(declaration.predicate.terms.len()),
779            }),
780    );
781    objects.extend(program.learnable_rules.iter().map(|rule| ObjectDescriptor {
782        kind: ResolvedSourceObjectKind::LearnableRule,
783        primary_name: Some(rule.head.predicate.clone()),
784        arity: Some(rule.head.terms.len()),
785    }));
786    objects.extend(
787        program
788            .directives
789            .set_pragma_names()
790            .into_iter()
791            .map(|name| ObjectDescriptor {
792                kind: ResolvedSourceObjectKind::Directive,
793                primary_name: Some(name.to_string()),
794                arity: None,
795            }),
796    );
797    objects
798}
799
800fn sha256_prefixed(bytes: &[u8]) -> String {
801    let mut digest = Sha256::new();
802    digest.update(bytes);
803    format!("sha256:{}", hex_digest(digest.finalize()))
804}
805
806fn hex_digest(bytes: impl AsRef<[u8]>) -> String {
807    let bytes = bytes.as_ref();
808    let mut output = String::with_capacity(bytes.len() * 2);
809    for byte in bytes {
810        use std::fmt::Write as _;
811        write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
812    }
813    output
814}