mant-query 0.11.0

Bounded semantic queries over existing ManT document snapshots
Documentation
//! Excerpt DTOs for source-ordered selections resolved by shared selector policy.
use super::{ProjectionError, TLDR_TITLE, semantics_complete};
use crate::{
    ResolvedContent,
    selectors::{
        DOCUMENT_ROOT_TITLE, DocumentSelectorIndex, LocatedBreadcrumb, LocatedNode, TLDR_ID,
        collect_root_entries, collect_sections,
    },
};
use mant_ir::{DOCUMENT_ROOT_ID, OutlinePath};
use mant_protocol::{
    ContentSelector, ExcerptSchema, ExcerptSelection, MAX_NODE_SELECTORS, OutlineNodeReference,
    OutlineReference, OutlineTrail, QueryExcerpt,
};
use std::collections::HashSet;

pub(super) fn selector_matches(selector: &ContentSelector, path: &OutlinePath, id: &str) -> bool {
    match selector {
        ContentSelector::Path { path: selected } => selected.as_str() == path.to_string(),
        ContentSelector::Id { id: selected } => selected.as_str() == id,
    }
}

/// Select tldr, document-root content, or complete section subtrees by path or ID.
///
/// Duplicate selections and descendants of another selected node are omitted.
/// The result always follows source order, independent of argument order.
///
/// # Errors
///
/// Returns an error when no content exists or any selector is empty or unknown.
pub fn select_excerpt(
    query: &ResolvedContent,
    selectors: &[ContentSelector],
) -> Result<QueryExcerpt, ProjectionError> {
    if selectors.is_empty() {
        return Err(ProjectionError::EmptySelection);
    }
    if selectors.len() > MAX_NODE_SELECTORS {
        return Err(ProjectionError::TooManySelections {
            maximum: MAX_NODE_SELECTORS,
        });
    }
    for selector in selectors {
        selector
            .validate()
            .map_err(|_| ProjectionError::InvalidSelector)?;
    }
    if query.tldr.is_none() && query.document.is_none() {
        return Err(ProjectionError::MissingContent {
            document: query.label.clone(),
        });
    }
    let mut located = Vec::new();
    if let Some(manual) = &query.document {
        collect_root_entries(&manual.blocks, &mut located);
        collect_sections(&manual.sections, &[], &[], &mut located);
    }
    let index = DocumentSelectorIndex::new(&located);

    let (tldr_selected, document_root_selected, mut selected) =
        resolve_excerpt_candidates(query, selectors, &index)?;
    let selected_sections = selected
        .iter()
        .filter(|candidate| candidate.is_section())
        .map(|candidate| candidate.coordinates().to_vec())
        .collect::<Vec<_>>();
    selected.retain(|candidate| {
        if document_root_selected && candidate.path().is_document_root_entry() {
            return false;
        }
        !selected_sections.iter().any(|ancestor| {
            if candidate.is_section() {
                ancestor != candidate.coordinates()
                    && is_ancestor(ancestor, candidate.coordinates())
            } else {
                ancestor == candidate.coordinates()
                    || is_ancestor(ancestor, candidate.coordinates())
            }
        })
    });
    selected.sort_by_key(|candidate| candidate.order());

    let document = if selected.is_empty() && !document_root_selected {
        None
    } else {
        query.document.as_ref()
    };
    let mut selections = Vec::new();
    if let (true, Some(document)) = (tldr_selected, query.tldr.clone()) {
        selections.push(ExcerptSelection::Tldr {
            outline: OutlineTrail {
                ancestors: Vec::new(),
                node: OutlineNodeReference::Tldr {
                    path: OutlinePath::Tldr.to_string().into(),
                    id: TLDR_ID.into(),
                    title: TLDR_TITLE.to_owned(),
                },
            },
            document,
        });
    }
    if let (true, Some(document)) = (document_root_selected, query.document.as_ref()) {
        selections.push(ExcerptSelection::DocumentRoot {
            outline: OutlineTrail {
                ancestors: Vec::new(),
                node: OutlineNodeReference::DocumentRoot {
                    path: OutlinePath::DocumentRoot.to_string().into(),
                    id: DOCUMENT_ROOT_ID.into(),
                    title: DOCUMENT_ROOT_TITLE.to_owned(),
                },
            },
            heading: document.heading.clone(),
            blocks: document.blocks.clone(),
        });
    }
    selections.extend(selected.into_iter().map(LocatedNode::selection));

    Ok(QueryExcerpt {
        display_title: query
            .document
            .as_ref()
            .and_then(mant_ir::Document::display_title)
            .map(std::borrow::Cow::into_owned),
        schema: ExcerptSchema::V0Dot11,
        label: query.label.clone(),
        address: query.address.clone(),
        semantics_complete: document
            .is_none_or(|document| semantics_complete(&document.diagnostics)),
        producer: document.map(mant_protocol::Producer::for_document),
        source: document.map(|document| document.source.clone()),
        meta: document.map(|document| document.meta.clone()),
        diagnostics: document
            .map(|document| document.diagnostics.clone())
            .unwrap_or_default(),
        selections,
    })
}

fn resolve_excerpt_candidates<'a>(
    query: &ResolvedContent,
    selectors: &[ContentSelector],
    index: &DocumentSelectorIndex<'a>,
) -> Result<(bool, bool, Vec<&'a LocatedNode<'a>>), ProjectionError> {
    let mut tldr_selected = false;
    let mut document_root_selected = false;
    let mut selected_paths = HashSet::new();
    let mut selected = Vec::new();
    for selector in selectors {
        if selector_matches(selector, &OutlinePath::Tldr, TLDR_ID) && query.tldr.is_some() {
            index.validate_synthetic_identity(&query.label, selector, TLDR_ID, "0")?;
            tldr_selected = true;
            continue;
        }
        if selector_matches(selector, &OutlinePath::DocumentRoot, DOCUMENT_ROOT_ID)
            && query
                .document
                .as_ref()
                .is_some_and(|document| document.heading.is_some() || !document.blocks.is_empty())
        {
            index.validate_synthetic_identity(&query.label, selector, DOCUMENT_ROOT_ID, "root")?;
            document_root_selected = true;
            continue;
        }
        let candidate = index.resolve(&query.label, selector)?;
        if selected_paths.insert(candidate.path().clone()) {
            selected.push(candidate);
        }
    }
    Ok((tldr_selected, document_root_selected, selected))
}

impl LocatedNode<'_> {
    pub(crate) fn selection(&self) -> ExcerptSelection {
        match self {
            Self::Section {
                path,
                breadcrumbs,
                section,
                ..
            } => ExcerptSelection::DocumentSection {
                outline: OutlineTrail {
                    ancestors: project_breadcrumbs(breadcrumbs),
                    node: OutlineNodeReference::DocumentSection {
                        path: path.to_string().into(),
                        id: section.id.clone(),
                        title: section.heading.plain_text(),
                    },
                },
                section: (*section).clone(),
            },
            Self::Entry {
                path,
                title,
                breadcrumbs,
                entry,
                ..
            } => ExcerptSelection::DocumentEntry {
                outline: OutlineTrail {
                    ancestors: project_breadcrumbs(breadcrumbs),
                    node: {
                        let identity = entry
                            .owner()
                            .facts()
                            .expect("located entries have identities");
                        OutlineNodeReference::DocumentEntry {
                            path: path.to_string().into(),
                            id: identity.id.clone(),
                            title: title.clone(),
                            entry_kind: identity.kind,
                            case: identity.case,
                            names: entry.names().to_vec(),
                        }
                    },
                },
                entry: entry.content(),
            },
        }
    }
}

pub(crate) fn project_breadcrumbs(breadcrumbs: &[LocatedBreadcrumb]) -> Vec<OutlineReference> {
    breadcrumbs
        .iter()
        .map(|breadcrumb| OutlineReference {
            path: breadcrumb.path.to_string().into(),
            id: breadcrumb.id.clone(),
            title: breadcrumb.title.clone(),
        })
        .collect()
}

fn is_ancestor(ancestor: &[usize], descendant: &[usize]) -> bool {
    ancestor.len() < descendant.len() && descendant.starts_with(ancestor)
}