ggen-engine 26.10.11

SPARQL-in-Tera code generation engine (vendored, renamed from praxis/crates/ggen)
//! Load a project's graph without running the full sync pipeline.
//!
//! `sync()` and `generation_rules::run()` each load the project graph as
//! step one of a much larger pipeline (template discovery, rendering,
//! writing, receipt chaining). A caller that only wants "the graph, as the
//! CLI would build it" — e.g. an ad-hoc query tool — has no way to get that
//! without either duplicating the loading logic (the exact drift this
//! module exists to prevent) or paying for the rest of the pipeline it
//! doesn't need.
//!
//! This module is the shared entry point for that narrower need. It
//! performs the same schema dispatch and ontology+imports+packs resolution
//! `sync`/`generation_rules` do — reusing
//! [`crate::sync::new_graph_engine`] and [`crate::sync::read_ontology_file`]
//! — but stops once the graph is built, before any template/render/write
//! stage. Unlike `sync`, pack resolution here is read-only
//! ([`crate::pack::resolve_read_only`]): it never clones a git pack or
//! writes a clone-cache/pin file, so a caller relying on this module for a
//! read-only contract (e.g. a SPARQL query preview) cannot trigger network
//! I/O or filesystem writes merely by loading the graph.

use std::path::Path;
use std::sync::Arc;

use crate::error::Result;
use crate::graph::{GraphEngine, TurtleDocument};
use crate::schema_dispatch::{self, ParsedGgenToml};
use crate::sync::{new_graph_engine, read_ontology_file, EngineKind};

/// Load `root`'s project graph (ontology + imports + packs, whichever apply
/// to its `ggen.toml` schema) using the default [`EngineKind`], without
/// running template discovery, rendering, or writes.
///
/// This is a read-only entry point: unlike `sync run`'s Resolve stage, it
/// never performs network I/O or writes a git pack's clone cache. For the
/// frontmatter schema, packs are resolved via
/// [`crate::pack::resolve_read_only`] — a `PackRef::Git` pack resolves only
/// from an already-cached, correctly-pinned clone; an uncached/mismatched
/// git pack is refused (`[FM-PACK-012]`) rather than cloned. Callers that
/// need sync's full (network-permitting) pack resolution should call
/// `sync`/`generation_rules::run` instead.
///
/// # Errors
/// Propagates `ggen.toml` classification/parse failures, ontology
/// read/parse failures, and (for the frontmatter schema) pack resolution
/// failures — including `[FM-PACK-012]` for an uncached git pack, which
/// `sync run`'s own Resolve stage would instead clone.
pub fn load_for_query(root: &Path) -> Result<Arc<dyn GraphEngine>> {
    load_for_query_with_engine(root, EngineKind::default())
}

/// As [`load_for_query`], with an explicit engine choice.
///
/// # Errors
/// See [`load_for_query`].
pub fn load_for_query_with_engine(root: &Path, engine: EngineKind) -> Result<Arc<dyn GraphEngine>> {
    let graph: Arc<dyn GraphEngine> = new_graph_engine(engine)?;
    let sources = match schema_dispatch::load(root)? {
        ParsedGgenToml::DeclarativeRules(manifest) => {
            let mut sources = Vec::with_capacity(1 + manifest.ontology.imports.len());
            let ontology_path = root.join(&manifest.ontology.source);
            sources.push(read_ontology_file(root, &ontology_path)?);
            for import in &manifest.ontology.imports {
                let import_path = root.join(import);
                sources.push(read_ontology_file(root, &import_path)?);
            }
            sources
        }
        ParsedGgenToml::Frontmatter(config) => {
            let packs = crate::pack::resolve_read_only(&config, root)?;
            let ontology_path = root.join(&config.ontology.source);
            let mut sources = Vec::with_capacity(
                2 + packs
                    .iter()
                    .map(|p| 1 + p.extra_ontology_paths.len())
                    .sum::<usize>(),
            );
            sources.push(read_ontology_file(root, &ontology_path)?);
            for import in &config.ontology.imports {
                sources.push(read_ontology_file(root, &root.join(import))?);
            }
            for pack in &packs {
                sources.push(read_ontology_file(root, &pack.ontology_path)?);
                for (declared, extra_path) in &pack.extra_ontology_paths {
                    let (_, content) = read_ontology_file(root, extra_path)?;
                    sources.push((declared.clone(), content));
                }
            }
            let pack_topology_ttl = crate::pack_scope::topology_turtle(&packs);
            if !pack_topology_ttl.is_empty() {
                sources.push(("urn:ggen:pack-topology".to_string(), pack_topology_ttl));
            }
            sources
        }
    };
    let documents: Vec<TurtleDocument<'_>> = sources
        .iter()
        .map(|(label, content)| TurtleDocument::new(label, content))
        .collect();
    let _receipt = graph.insert_turtle_documents(&documents)?;
    Ok(graph)
}