///|
/// Attach the draft conceptual graph to its existing unit metadata owner.
/// An occupied owner, including another graph, is never overwritten implicitly.
pub fn IRUnit::with_conceptual_graph(
  self : IRUnit,
  graph : @model.Graph,
) -> IRUnit raise {
  for i, entry in self.metadata {
    if entry.node_id == "unit:" + self.id {
      raise Invalid(
        "conceptual.metadata_owner_conflict",
        "/existingMetadata/" + i.to_string() + "/nodeId",
      )
    }
  }
  self.validate()
  let result = {
    ..self,
    metadata: [
      ..self.metadata,
      { node_id: "unit:" + self.id, data: graph.to_ion(), },
    ],
  }
  result.validate()
  result
}

///|
/// Read the graph at the unit owner, leaving unrelated metadata untouched.
/// Existing graph-free profiles return None. A recognized malformed graph fails.
pub fn IRUnit::conceptual_graph(self : IRUnit) -> @model.Graph? raise {
  let mut owned : @ion.IonValue? = None
  for i, entry in self.metadata {
    if entry.node_id == "unit:" + self.id {
      if owned is Some(_) {
        raise Invalid(
          "conceptual.metadata_owner_conflict",
          "/metadata/" + i.to_string() + "/node_id",
        )
      }
      owned = Some(entry.data)
    }
  }
  guard owned is Some(value) else { return None }
  guard value.kind() is Struct(fields) else { return None }
  let conceptual = fields.any(f => {
    f.name().text() == Some("profile") &&
    (match f.value().kind() {
      String(s) => s.has_prefix("morphir-conceptual/")
      _ => false
    })
  })
  if conceptual {
    Some(@model.Graph::decode(value))
  } else {
    None
  }
}

///|
/// Extract public Library signatures while retaining the complete conceptual
/// graph and carrier facts. Metadata whose owner disappears makes projection
/// lossy and is refused. This does not evaluate or lower conceptual content.
pub fn IRUnit::conceptual_specification(self : IRUnit) -> IRUnit raise {
  self.validate()
  ignore(self.conceptual_graph())
  let distribution = match self.distribution.distribution {
    Specs(_, _, _) => return self
    Application(_, _, _, _) =>
      raise Invalid(
        "conceptual.unsupported_projection", "Application projection requires a separate linking policy",
      )
    Library(name, dependencies, definition) => {
      let modules : Map[@ir.ModuleName, @ir.ModuleSpecification] = Map([])
      for module_name, module_entry in definition.modules {
        if module_entry.access != @ir.Access::Public {
          continue
        }
        let types : Map[@ir.Name, @ir.Documented[@ir.TypeSpecification]] = Map([])
        let values : Map[@ir.Name, @ir.Documented[@ir.ValueSpecification]] = Map([],
        )
        for name, entry in module_entry.value.types {
          if entry.access != @ir.Access::Public {
            continue
          }
          let body = match entry.value.value {
            TypeAliasDefinition(params, body) =>
              @ir.TypeSpecificationBody::TypeAliasSpecification(params, body)
            CustomTypeDefinition(params, constructors) =>
              if constructors.access == @ir.Access::Public {
                @ir.TypeSpecificationBody::CustomTypeSpecification(
                  params,
                  constructors.value,
                )
              } else {
                @ir.TypeSpecificationBody::OpaqueTypeSpecification(params)
              }
            IncompleteTypeDefinition(_, _, _) =>
              raise Invalid(
                "data.lossy_conversion",
                "Public incomplete type has no complete specification: " +
                name.to_canonical(),
              )
          }
          types[name] = {
            doc: entry.value.doc,
            value: { annotations: [], body, },
          }
        }
        for name, entry in module_entry.value.values {
          if entry.access != @ir.Access::Public {
            continue
          }
          let specification = match entry.value.value.to_specification() {
            Some(specification) => specification
            None =>
              raise Invalid(
                "data.lossy_conversion",
                "Public value has no output type: " + name.to_canonical(),
              )
          }
          values[name] = { doc: entry.value.doc, value: specification, }
        }
        modules[module_name] = {
          annotations: [],
          types,
          values,
          doc: module_entry.value.doc,
        }
      }
      @ir.Distribution::Specs(name, dependencies, { modules, })
    }
  }
  let distribution = @ir.IRFile::new(
    distribution,
    format_version=self.distribution.format_version,
  )
  let owners = @identities.nodes(self.id, distribution)
  for entry in self.metadata {
    if !owners.contains(entry.node_id) {
      raise Invalid(
        "data.lossy_conversion",
        "Projection removes metadata owner: " + entry.node_id,
      )
    }
  }
  let result = {
    ..self,
    distribution,
    metadata: self.metadata.copy(),
    annotations: self.annotations.copy(),
    provenance: self.provenance.copy(),
  }
  result.validate()
  result
}