master
Drew Owens Share frozen native Rust mechanical development checkpoint and qualification scope b2d7cdb 26d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
use crate::*;
use std::collections::{BTreeMap, BTreeSet, VecDeque};

pub fn generate(snapshot: &AssemblySnapshot, options: Options) -> Result<Bom> {
    generate_with_limits(snapshot, options, Limits::default())
}

pub fn generate_with_limits(
    snapshot: &AssemblySnapshot,
    options: Options,
    limits: Limits,
) -> Result<Bom> {
    let maximum = Limits::default();
    if limits.max_depth > maximum.max_depth
        || limits.max_component_visits == 0
        || limits.max_component_visits > maximum.max_component_visits
        || limits.max_physical_body_instances > maximum.max_physical_body_instances
    {
        return Err(Error::new(
            "invalid_limits",
            "BOM limits may only reduce the hard limits",
        ));
    }
    bounded_serialized(snapshot, MAX_SNAPSHOT_BYTES)?;
    if snapshot.target.document_id.is_empty()
        || snapshot.target.revision.is_empty()
        || snapshot.witness.session_id.is_empty()
        || snapshot.witness.design_id.is_empty()
        || snapshot.witness.target != snapshot.target
        || !snapshot.witness.complete
        || !snapshot.witness.read_errors.is_empty()
        || snapshot.witness.calculation_accuracy != CalculationAccuracy::High
    {
        return Err(Error::new(
            "source_incomplete",
            "A complete current-target high-accuracy source witness is required",
        ));
    }
    let config = snapshot.configuration.validate(&snapshot.target)?;
    let graph = Graph::new(snapshot)?;
    for column in snapshot
        .configuration
        .table
        .iter()
        .flat_map(|table| &table.columns)
    {
        if let Some(source) = &column.source {
            graph.validate_configuration_source(source)?;
        }
    }
    let mut run = Run {
        graph: &graph,
        options,
        limits,
        visited: 0,
        physical: PhysicalProperties::default(),
        body_instances: 0,
        parts: Vec::new(),
        part_indexes: BTreeMap::new(),
        instances: Vec::new(),
        excluded: Vec::new(),
        suppressed_sources: &config.suppressed_sources,
        output_budget: MAX_SNAPSHOT_BYTES,
    };
    run.purchasing(None, None, 0, &mut Vec::new())?;
    run.physical_walk(None, 0, &mut Vec::new())?;
    let total_purchasing_instances = run.parts.iter().map(|row| row.quantity).sum();
    let result = Bom {
        target: snapshot.target.clone(), complete: true,
        configuration: config.provenance, configuration_suppression: config.suppression_cells,
        part_count: run.parts.len(), total_purchasing_instances,
        parts: run.parts, instances: run.instances, excluded: run.excluded,
        physical: PhysicalTotals {
            mass: run.physical.mass_kg, volume: run.physical.volume_mm3, body_instances: run.body_instances,
            scope: "all unsuppressed direct-body instances; BOM exclusions do not remove physical mass".into(),
            material_basis: "Fusion assigned/default material; supplier provenance does not certify modeled material density".into(),
        },
        units: Units { mass: "kg".into(), volume: "mm3".into() },
        calculation_accuracy: "high".into(),
        identity_scope: "running add-in session; supplier metadata persists in native archives".into(),
    };
    bounded_serialized(&result, MAX_SNAPSHOT_BYTES)?;
    Ok(result)
}

struct Graph<'a> {
    source: &'a AssemblySnapshot,
    components: BTreeMap<&'a str, &'a Component>,
    occurrences: BTreeMap<&'a str, &'a Occurrence>,
}

impl<'a> Graph<'a> {
    fn new(source: &'a AssemblySnapshot) -> Result<Self> {
        if source.components.is_empty()
            || source.components.len() > MAX_GRAPH_ENTITIES
            || source.occurrences.len() > MAX_GRAPH_ENTITIES
        {
            return Err(Error::new(
                "bom_budget",
                "Assembly entity inventory exceeds budget",
            ));
        }
        let mut graph = Self {
            source,
            components: BTreeMap::new(),
            occurrences: BTreeMap::new(),
        };
        let mut paths = BTreeSet::new();
        let mut aliases = 0;
        for component in &source.components {
            component.metadata.validate()?;
            graph.validate_reference(&component.reference, "adsk::fusion::Component")?;
            if component.direct_bodies.len() > MAX_GRAPH_ENTITIES {
                return Err(Error::new(
                    "bom_budget",
                    "Component body inventory exceeds budget",
                ));
            }
            for properties in &component.direct_bodies {
                properties.validate()?;
            }
            if let Some(properties) = component.subtree_properties {
                properties.validate()?;
            }
            for id in std::iter::once(&component.id).chain(&component.aliases) {
                aliases += 1;
                if aliases > MAX_GRAPH_ENTITIES {
                    return Err(Error::new(
                        "bom_budget",
                        "Identity alias inventory exceeds budget",
                    ));
                }
                if id.is_empty() || graph.components.insert(id, component).is_some() {
                    return Err(Error::new(
                        "ambiguous_identity",
                        "Missing or ambiguous component definition identity",
                    ));
                }
            }
        }
        let root = graph.component(&source.root.component_id)?;
        if root
            .reference
            .root_component_id
            .as_deref()
            .and_then(|id| graph.components.get(id))
            .map(|component| component.id.as_str())
            != Some(root.id.as_str())
        {
            return Err(Error::new(
                "source_incomplete",
                "Root component reference lacks the matching session identity",
            ));
        }
        for occurrence in &source.occurrences {
            if occurrence.id.is_empty()
                || occurrence.path.is_empty()
                || occurrence.path == "<root>"
                || !paths.insert(&occurrence.path)
                || graph
                    .occurrences
                    .insert(&occurrence.id, occurrence)
                    .is_some()
            {
                return Err(Error::new(
                    "ambiguous_identity",
                    "Occurrence identities and full paths must be unique",
                ));
            }
            let component = graph.component(&occurrence.component_id)?;
            graph.validate_reference(&occurrence.reference, "adsk::fusion::Occurrence")?;
            if occurrence.reference.occurrence_path.as_deref() != Some(occurrence.path.as_str()) {
                return Err(Error::new(
                    "source_incomplete",
                    "Occurrence reference path does not match its instance",
                ));
            }
            graph.validate_source(&occurrence.source)?;
            if occurrence.source.object_type != "adsk::fusion::Occurrence"
                || occurrence.source.owner_scope != OwnerScope::Component
                || graph.component(&occurrence.source.owner_id)?.id != component.id
            {
                return Err(Error::new(
                    "configuration_source_mismatch",
                    "Occurrence source owner/type does not match its component",
                ));
            }
            if occurrence.direct_bodies.len() != component.direct_bodies.len() {
                return Err(Error::new(
                    "source_incomplete",
                    "Occurrence direct-body count differs from its definition",
                ));
            }
            for properties in &occurrence.direct_bodies {
                properties.validate()?;
            }
        }
        // Native occurrence instances form a tree, even though definitions are
        // reused. A shared occurrence node would otherwise double-count one body.
        let mut parents = BTreeMap::<&str, usize>::new();
        for child in source
            .root
            .children
            .iter()
            .chain(source.occurrences.iter().flat_map(|o| &o.children))
        {
            graph.occurrence(child)?;
            *parents.entry(child).or_default() += 1;
        }
        if graph
            .occurrences
            .keys()
            .any(|id| parents.get(id) != Some(&1))
        {
            return Err(Error::new(
                "invalid_assembly_graph",
                "Every occurrence must have exactly one containment parent",
            ));
        }
        let mut reached = BTreeSet::new();
        let mut queue: VecDeque<&str> = source.root.children.iter().map(String::as_str).collect();
        while let Some(id) = queue.pop_front() {
            if !reached.insert(id) {
                return Err(Error::new(
                    "cyclic_assembly",
                    "Occurrence containment is cyclic",
                ));
            }
            queue.extend(graph.occurrence(id)?.children.iter().map(String::as_str));
        }
        if reached.len() != graph.occurrences.len() {
            return Err(Error::new(
                "invalid_assembly_graph",
                "Occurrence inventory has unreachable nodes",
            ));
        }
        Ok(graph)
    }

    fn component(&self, id: &str) -> Result<&'a Component> {
        self.components.get(id).copied().ok_or_else(|| {
            Error::new(
                "source_incomplete",
                format!("Unknown component identity: {id}"),
            )
        })
    }
    fn occurrence(&self, id: &str) -> Result<&'a Occurrence> {
        self.occurrences.get(id).copied().ok_or_else(|| {
            Error::new(
                "source_incomplete",
                format!("Unknown occurrence identity: {id}"),
            )
        })
    }
    fn validate_reference(&self, reference: &EntityRef, object_type: &str) -> Result<()> {
        if reference.document_id != self.source.target.document_id
            || reference.revision != self.source.target.revision
            || reference.object_type != object_type
        {
            return Err(Error::new(
                "source_incomplete",
                "Entity reference has a foreign target/revision/type",
            ));
        }
        Ok(())
    }
    fn validate_source(&self, source: &SourceLink) -> Result<()> {
        if source.entity_id.is_empty()
            || source.owner_id.is_empty()
            || source.object_type.is_empty()
            || !(source.object_type.starts_with("adsk::fusion::")
                || source.object_type.starts_with("adsk::core::"))
            || (source.object_type.ends_with("::Occurrence")
                && source.object_type != "adsk::fusion::Occurrence")
            || source.occurrence_id.as_ref().is_some_and(String::is_empty)
        {
            return Err(Error::new(
                "configuration_source_mismatch",
                "Controlled-source identity is incomplete",
            ));
        }
        if source.owner_scope == OwnerScope::Component {
            self.component(&source.owner_id)?;
        } else if source.owner_id != self.source.witness.design_id
            || source.object_type != "adsk::fusion::UserParameter"
        {
            return Err(Error::new(
                "configuration_source_mismatch",
                "Design-owned source is not a user parameter of this Design",
            ));
        }
        Ok(())
    }

    fn validate_configuration_source(&self, source: &SourceLink) -> Result<()> {
        self.validate_source(source)?;
        if source.object_type != "adsk::fusion::Occurrence"
            && self
                .occurrences
                .values()
                .any(|occurrence| occurrence.source.entity_id == source.entity_id)
        {
            return Err(Error::new(
                "configuration_source_mismatch",
                "Controlled source type contradicts an observed occurrence identity",
            ));
        }
        if source.object_type == "adsk::fusion::Occurrence" {
            if source.owner_scope != OwnerScope::Component {
                return Err(Error::new(
                    "configuration_source_mismatch",
                    "Occurrence source must have component ownership",
                ));
            }
            let owner = self.component(&source.owner_id)?;
            let mut found = false;
            for occurrence in self.occurrences.values().filter(|occurrence| {
                occurrence.source.entity_id == source.entity_id
                    && (source.occurrence_id.is_none()
                        || source.occurrence_id == occurrence.source.occurrence_id)
            }) {
                found = true;
                if occurrence.source.object_type != source.object_type
                    || occurrence.source.owner_scope != source.owner_scope
                    || self.component(&occurrence.source.owner_id)?.id != owner.id
                {
                    return Err(Error::new(
                        "configuration_source_mismatch",
                        "Controlled occurrence source contradicts its observed owner/type",
                    ));
                }
            }
            if !found {
                return Err(Error::new(
                    "configuration_source_mismatch",
                    "Controlled occurrence source has no matching observed entity/context",
                ));
            }
        }
        Ok(())
    }
}

struct Run<'a, 'b> {
    graph: &'b Graph<'a>,
    options: Options,
    limits: Limits,
    visited: usize,
    physical: PhysicalProperties,
    body_instances: usize,
    parts: Vec<PartRow>,
    part_indexes: BTreeMap<&'a str, usize>,
    instances: Vec<InstanceRow>,
    excluded: Vec<Exclusion>,
    suppressed_sources: &'b [&'a SourceLink],
    output_budget: usize,
}

impl<'a> Run<'a, '_> {
    fn charge<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
        // Reserve bytes before cloning repeated metadata into output rows.
        // The fixed allowance covers field names, brackets and numeric values;
        // final exact serialization is checked as well.
        let bytes = serialized_len(value, self.output_budget)?.saturating_add(512);
        self.output_budget = self.output_budget.checked_sub(bytes).ok_or_else(|| {
            Error::new("bom_budget", "BOM materialization exceeds its byte budget")
        })?;
        Ok(())
    }
    fn node(
        &self,
        occurrence: Option<&'a Occurrence>,
    ) -> Result<(&'a Component, &'a [String], &'a str)> {
        match occurrence {
            Some(occurrence) => Ok((
                self.graph.component(&occurrence.component_id)?,
                &occurrence.children,
                &occurrence.path,
            )),
            None => Ok((
                self.graph.component(&self.graph.source.root.component_id)?,
                &self.graph.source.root.children,
                "<root>",
            )),
        }
    }
    fn visit(&mut self, component: &Component, depth: usize, ancestry: &[&str]) -> Result<()> {
        self.visited += 1;
        if depth > self.limits.max_depth || self.visited > self.limits.max_component_visits {
            return Err(Error::new(
                "bom_budget",
                "Assembly exceeds combined purchasing/physical traversal budget",
            ));
        }
        if ancestry.contains(&component.id.as_str()) {
            return Err(Error::new(
                "cyclic_assembly",
                "Assembly contains cyclic component containment",
            ));
        }
        Ok(())
    }
    fn suppression(&self, occurrence: &Occurrence) -> Option<&'static str> {
        if occurrence.timeline_suppressed == Some(true) {
            return Some("suppressed");
        }
        if self.suppressed_sources.iter().any(|source| {
            source.object_type == "adsk::fusion::Occurrence"
                && source.owner_scope == OwnerScope::Component
                && self
                    .graph
                    .components
                    .get(source.owner_id.as_str())
                    .map(|component| &component.id)
                    == self
                        .graph
                        .components
                        .get(occurrence.source.owner_id.as_str())
                        .map(|component| &component.id)
                && source.entity_id == occurrence.source.entity_id
                && (source.occurrence_id.is_none()
                    || source.occurrence_id == occurrence.source.occurrence_id)
        }) {
            return Some("configuration_suppressed");
        }
        None
    }
    fn purchasing(
        &mut self,
        occurrence: Option<&'a Occurrence>,
        parent_path: Option<&str>,
        depth: usize,
        ancestry: &mut Vec<&'a str>,
    ) -> Result<()> {
        let (component, children, path) = self.node(occurrence)?;
        self.visit(component, depth, ancestry)?;
        let reason = occurrence.and_then(|o| self.suppression(o)).or_else(|| {
            (component.metadata.exclude_from_bom == Some(true)).then_some("excludeFromBom")
        });
        if let Some(reason) = reason {
            self.charge(&(path, reason))?;
            self.excluded.push(Exclusion {
                path: path.into(),
                reason: reason.into(),
            });
            return Ok(());
        }
        let purchased =
            component.metadata.as_purchased == Some(true) && !self.options.explode_purchased;
        self.charge(&(
            path,
            parent_path,
            &component.id,
            &component.metadata,
            &component.reference,
            occurrence.map(|o| &o.reference),
        ))?;
        self.instances.push(InstanceRow {
            path: path.into(),
            parent_path: parent_path.map(str::to_owned),
            depth,
            component_id: component.id.clone(),
            metadata: component.metadata.clone(),
            body_count: component.direct_bodies.len(),
            child_count: children.len(),
            as_purchased: purchased,
            component_ref: component.reference.clone(),
            occurrence_ref: occurrence.map(|o| o.reference.clone()),
        });
        if !component.direct_bodies.is_empty() || purchased {
            let index = if let Some(index) = self.part_indexes.get(component.id.as_str()) {
                *index
            } else {
                let mut properties = PhysicalProperties::default();
                if purchased {
                    properties = component.subtree_properties.ok_or_else(|| Error::new("source_incomplete", "Collapsed purchased component lacks native subtree physical properties"))?;
                } else {
                    for body in &component.direct_bodies {
                        properties.add(*body)?;
                    }
                }
                self.charge(&(&component.id, &component.metadata))?;
                let index = self.parts.len();
                self.parts.push(PartRow {
                    component_id: component.id.clone(),
                    metadata: component.metadata.clone(),
                    quantity: 0,
                    occurrence_paths: Vec::new(),
                    scope: if purchased {
                        "component_subtree"
                    } else {
                        "direct_component_bodies"
                    }
                    .into(),
                    unit_mass: properties.mass_kg,
                    unit_volume: properties.volume_mm3,
                });
                self.part_indexes.insert(&component.id, index);
                index
            };
            self.charge(&path)?;
            self.parts[index].quantity += 1;
            self.parts[index].occurrence_paths.push(path.into());
        }
        if !purchased {
            ancestry.push(&component.id);
            for child in children {
                self.purchasing(
                    Some(self.graph.occurrence(child)?),
                    Some(path),
                    depth + 1,
                    ancestry,
                )?;
            }
            ancestry.pop();
        }
        Ok(())
    }
    fn physical_walk(
        &mut self,
        occurrence: Option<&'a Occurrence>,
        depth: usize,
        ancestry: &mut Vec<&'a str>,
    ) -> Result<()> {
        let (component, children, _) = self.node(occurrence)?;
        self.visit(component, depth, ancestry)?;
        let bodies = occurrence
            .map(|o| o.direct_bodies.as_slice())
            .unwrap_or(&component.direct_bodies);
        for body in bodies {
            self.body_instances += 1;
            if self.body_instances > self.limits.max_physical_body_instances {
                return Err(Error::new(
                    "bom_budget",
                    "Physical body-instance budget exceeded",
                ));
            }
            self.physical.add(*body)?;
        }
        ancestry.push(&component.id);
        for child in children {
            let child = self.graph.occurrence(child)?;
            if self.suppression(child).is_none() {
                self.physical_walk(Some(child), depth + 1, ancestry)?;
            }
        }
        ancestry.pop();
        Ok(())
    }
}