616 lines
24 KiB
Rust
616 lines
24 KiB
Rust
//! Typed artifact lifecycle drafts; all authored values belong to an exact pair/facet.
|
|
use super::*;
|
|
use serde::{Deserialize, Serialize};
|
|
pub(super) const METADATA: [ArtifactFacet; 7] = [
|
|
ArtifactFacet::Name,
|
|
ArtifactFacet::Description,
|
|
ArtifactFacet::Owner,
|
|
ArtifactFacet::Relation,
|
|
ArtifactFacet::Uri,
|
|
ArtifactFacet::Revision,
|
|
ArtifactFacet::Fragment,
|
|
];
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum Raw {
|
|
Text(String),
|
|
Owner(String),
|
|
Relation(ArtifactRelation),
|
|
Optional { present: bool, text: String },
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum Event {
|
|
Pick(Layer, Option<Choice>),
|
|
Search(String),
|
|
Stage,
|
|
Reserve,
|
|
Input(FacetSelector, Raw),
|
|
Description(FacetSelector, text_editor::Action),
|
|
}
|
|
#[derive(Default)]
|
|
pub struct Editor {
|
|
pub oa: Option<Choice>,
|
|
pub sa: Option<Choice>,
|
|
pub search: String,
|
|
pub seeds: BTreeMap<Id, ArtifactLink>,
|
|
pub drafts: BTreeMap<FacetSelector, Raw>,
|
|
pub descriptions: BTreeMap<FacetSelector, text_editor::Content>,
|
|
}
|
|
#[derive(Default, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Saved {
|
|
oa: Option<Id>,
|
|
sa: Option<Id>,
|
|
seeds: Vec<ArtifactLink>,
|
|
drafts: Vec<(FacetSelector, Raw)>,
|
|
}
|
|
impl Saved {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.oa.is_none() && self.sa.is_none() && self.seeds.is_empty() && self.drafts.is_empty()
|
|
}
|
|
}
|
|
pub struct Proposal {
|
|
// The final selector addresses the actual mutation slot; the last selector
|
|
// retains the reviewed facet solely for conflict diagnostics.
|
|
pub slots: Vec<((Id, FacetSelector), Value, FacetSelector)>,
|
|
pub creations: Vec<ArtifactLink>,
|
|
pub candidates: BTreeSet<Id>,
|
|
}
|
|
pub(super) fn parts(facet: &FacetSelector) -> Option<(&KeyPair<Id>, ArtifactFacet)> {
|
|
if let FacetSelector::Artifact { keys, facet } = facet {
|
|
Some((keys, *facet))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
pub(super) fn supported(facet: &FacetSelector) -> bool {
|
|
parts(facet).is_some()
|
|
}
|
|
pub(super) fn selector(keys: &KeyPair<Id>, facet: ArtifactFacet) -> FacetSelector {
|
|
FacetSelector::Artifact {
|
|
keys: keys.clone(),
|
|
facet,
|
|
}
|
|
}
|
|
pub(super) fn key(keys: &KeyPair<Id>, layer: Layer) -> Option<Id> {
|
|
if layer == Layer::Oa { keys.oa } else { keys.sa }
|
|
}
|
|
pub(super) fn field(facet: ArtifactFacet) -> Option<&'static str> {
|
|
Some(match facet {
|
|
ArtifactFacet::Presence => return None,
|
|
ArtifactFacet::Name => "name",
|
|
ArtifactFacet::Description => "description",
|
|
ArtifactFacet::Owner => "owner",
|
|
ArtifactFacet::Relation => "relation",
|
|
ArtifactFacet::Uri => "uri",
|
|
ArtifactFacet::Revision => "revision",
|
|
ArtifactFacet::Fragment => "fragment",
|
|
})
|
|
}
|
|
pub(super) fn slot(id: Id, facet: ArtifactFacet) -> (Id, FacetSelector) {
|
|
(
|
|
id,
|
|
selector(
|
|
&KeyPair {
|
|
oa: None,
|
|
sa: Some(id),
|
|
},
|
|
if facet == ArtifactFacet::Presence {
|
|
ArtifactFacet::Owner
|
|
} else {
|
|
facet
|
|
},
|
|
),
|
|
)
|
|
}
|
|
pub(super) fn observation(record: Option<&ArtifactLink>, facet: ArtifactFacet) -> Value {
|
|
match record {
|
|
None => Value::String("NotApplicable".into()),
|
|
Some(record) => {
|
|
serde_json::json!({"Value":serde_json::to_value(record).expect("Artifact")[field(facet).unwrap_or("owner")]})
|
|
}
|
|
}
|
|
}
|
|
pub(super) fn raw(record: &ArtifactLink, facet: ArtifactFacet) -> Option<Raw> {
|
|
Some(match facet {
|
|
ArtifactFacet::Presence => return None,
|
|
ArtifactFacet::Name => Raw::Text(record.name.clone()),
|
|
ArtifactFacet::Description => Raw::Text(record.description.clone()),
|
|
ArtifactFacet::Uri => Raw::Text(record.uri.clone()),
|
|
ArtifactFacet::Owner => Raw::Owner(record.owner.to_string()),
|
|
ArtifactFacet::Relation => Raw::Relation(record.relation),
|
|
ArtifactFacet::Revision => Raw::Optional {
|
|
present: record.revision.is_some(),
|
|
text: record.revision.clone().unwrap_or_default(),
|
|
},
|
|
ArtifactFacet::Fragment => Raw::Optional {
|
|
present: record.fragment.is_some(),
|
|
text: record.fragment.clone().unwrap_or_default(),
|
|
},
|
|
})
|
|
}
|
|
fn compatible(facet: ArtifactFacet, raw: &Raw) -> bool {
|
|
matches!(
|
|
(facet, raw),
|
|
(
|
|
ArtifactFacet::Name | ArtifactFacet::Description | ArtifactFacet::Uri,
|
|
Raw::Text(_)
|
|
) | (ArtifactFacet::Owner, Raw::Owner(_))
|
|
| (ArtifactFacet::Relation, Raw::Relation(_))
|
|
| (
|
|
ArtifactFacet::Revision | ArtifactFacet::Fragment,
|
|
Raw::Optional { .. }
|
|
)
|
|
)
|
|
}
|
|
fn write(record: &mut ArtifactLink, facet: ArtifactFacet, raw: &Raw) -> Result<(), String> {
|
|
match (facet, raw) {
|
|
(ArtifactFacet::Name, Raw::Text(v)) => record.name = v.clone(),
|
|
(ArtifactFacet::Description, Raw::Text(v)) => record.description = v.clone(),
|
|
(ArtifactFacet::Uri, Raw::Text(v)) => record.uri = v.clone(),
|
|
(ArtifactFacet::Owner, Raw::Owner(v)) => {
|
|
record.owner = v
|
|
.parse()
|
|
.map_err(|_| "Choose a valid explicit artifact owner identity.")?
|
|
}
|
|
(ArtifactFacet::Relation, Raw::Relation(v)) => record.relation = *v,
|
|
(ArtifactFacet::Revision, Raw::Optional { present, text }) => {
|
|
record.revision = present.then(|| text.clone())
|
|
}
|
|
(ArtifactFacet::Fragment, Raw::Optional { present, text }) => {
|
|
record.fragment = present.then(|| text.clone())
|
|
}
|
|
_ => return Err("Artifact draft type disagrees with its selected metadata facet.".into()),
|
|
};
|
|
Ok(())
|
|
}
|
|
impl Editor {
|
|
pub fn pair(&self) -> KeyPair<Id> {
|
|
KeyPair {
|
|
oa: self.oa.as_ref().map(|c| c.id),
|
|
sa: self.sa.as_ref().map(|c| c.id),
|
|
}
|
|
}
|
|
pub fn select_pair(&mut self, p: &Project, facet: &FacetSelector) -> Result<(), String> {
|
|
let (keys, _) = parts(facet).ok_or("Choose an artifact address.")?;
|
|
self.oa = keys.oa.map(|id| self.choice(p, id));
|
|
self.sa = keys.sa.map(|id| self.choice(p, id));
|
|
Ok(())
|
|
}
|
|
pub fn choice(&self, p: &Project, id: Id) -> Choice {
|
|
if p.contains(id) {
|
|
return view::choice(p, id);
|
|
}
|
|
self.seeds.get(&id).map_or_else(
|
|
|| view::choice(p, id),
|
|
|a| Choice {
|
|
id,
|
|
label: format!("{} · reserved @{}", a.name, a.alias),
|
|
},
|
|
)
|
|
}
|
|
pub fn choices(&self, p: &Project, trace: &Trace, layer: Layer) -> Vec<Choice> {
|
|
let owner = if layer == Layer::Oa {
|
|
trace.oa
|
|
} else {
|
|
trace.sa
|
|
};
|
|
let selected = key(&self.pair(), layer);
|
|
let mut ids: BTreeSet<_> = p
|
|
.artifact_links
|
|
.values()
|
|
.filter(|a| p.layer_of(a.owner).is_none_or(|actual| actual == layer))
|
|
.map(|a| a.id)
|
|
.chain(
|
|
self.seeds
|
|
.values()
|
|
.filter(|a| a.owner == owner)
|
|
.map(|a| a.id),
|
|
)
|
|
.collect();
|
|
if let Some(id) = selected {
|
|
ids.insert(id);
|
|
}
|
|
if let Some(base) = trace.baseline.as_ref().and_then(|b| b.semantic.as_ref()) {
|
|
for fact in &base.facets {
|
|
if let Some((keys, _)) = parts(&fact.selector())
|
|
&& let Some(id) = key(keys, layer)
|
|
{
|
|
ids.insert(id);
|
|
}
|
|
}
|
|
}
|
|
let search = self.search.to_lowercase();
|
|
let mut choices: Vec<_> = ids
|
|
.into_iter()
|
|
.map(|id| {
|
|
let mut choice = self.choice(p, id);
|
|
if let Some(artifact) = p.artifact_links.get(&id) {
|
|
choice.label.push_str(&format!(
|
|
" · owner: {}{}",
|
|
p.named(artifact.owner),
|
|
if artifact.owner == owner {
|
|
""
|
|
} else {
|
|
" · not attached to reviewed owner"
|
|
}
|
|
));
|
|
}
|
|
choice
|
|
})
|
|
.filter(|c| Some(c.id) == selected || c.label.to_lowercase().contains(&search))
|
|
.collect();
|
|
choices.sort_by_cached_key(|c| c.label.to_lowercase());
|
|
choices
|
|
}
|
|
pub fn ensure(
|
|
&mut self,
|
|
p: &Project,
|
|
facet: &FacetSelector,
|
|
layer: Layer,
|
|
) -> Result<(), String> {
|
|
let (keys, which) = parts(facet).ok_or("Choose artifact metadata.")?;
|
|
let id = key(keys, layer).ok_or("Choose a destination artifact identity.")?;
|
|
let record = p
|
|
.artifact_links
|
|
.get(&id)
|
|
.or_else(|| self.seeds.get(&id))
|
|
.ok_or("Reserve a destination counterpart before reviewing its metadata.")?;
|
|
let value = raw(record, which).ok_or(
|
|
"Presence uses explicit Keep or Take; there is no deletion tombstone candidate.",
|
|
)?;
|
|
self.drafts.entry(facet.clone()).or_insert(value);
|
|
if which == ArtifactFacet::Description
|
|
&& let Some(Raw::Text(text)) = self.drafts.get(facet)
|
|
{
|
|
self.descriptions
|
|
.entry(facet.clone())
|
|
.or_insert_with(|| text_editor::Content::with_text(text));
|
|
}
|
|
Ok(())
|
|
}
|
|
pub fn update(
|
|
&mut self,
|
|
p: &Project,
|
|
trace: &Trace,
|
|
direction: TransitionDirection,
|
|
event: Event,
|
|
) -> Result<Option<Vec<FacetSelector>>, String> {
|
|
match event {
|
|
Event::Pick(layer, choice) => {
|
|
if layer == Layer::Oa {
|
|
self.oa = choice
|
|
} else {
|
|
self.sa = choice
|
|
}
|
|
}
|
|
Event::Search(search) => self.search = search,
|
|
Event::Stage => {
|
|
let keys = self.pair();
|
|
if keys.oa.is_none() && keys.sa.is_none() {
|
|
return Err("Choose at least one exact artifact identity.".into());
|
|
}
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
if key(&keys, layer)
|
|
.and_then(|id| p.layer_of(id))
|
|
.is_some_and(|actual| actual != layer)
|
|
{
|
|
return Err(
|
|
"Artifact pair identity belongs to the other perspective.".into()
|
|
);
|
|
}
|
|
}
|
|
capture_facet(
|
|
p,
|
|
trace.oa,
|
|
trace.sa,
|
|
&selector(&keys, ArtifactFacet::Presence),
|
|
)?;
|
|
return Ok(Some(
|
|
std::iter::once(ArtifactFacet::Presence)
|
|
.chain(METADATA)
|
|
.map(|facet| selector(&keys, facet))
|
|
.filter(|facet| capture_facet(p, trace.oa, trace.sa, facet).is_ok())
|
|
.collect(),
|
|
));
|
|
}
|
|
Event::Reserve => {
|
|
let source = key(&self.pair(), direction.source_layer())
|
|
.and_then(|id| p.artifact_links.get(&id))
|
|
.ok_or(
|
|
"Choose an existing source artifact before reserving its counterpart.",
|
|
)?;
|
|
let source_owner = if direction.source_layer() == Layer::Oa {
|
|
trace.oa
|
|
} else {
|
|
trace.sa
|
|
};
|
|
if source.owner != source_owner {
|
|
return Err(
|
|
"The source artifact is not attached to this reviewed owner.".into(),
|
|
);
|
|
}
|
|
let mut seed = source.clone();
|
|
seed.id = Id::new_v4();
|
|
seed.alias = format!("artifact_{}", seed.id.simple());
|
|
seed.owner = if direction.target_layer() == Layer::Oa {
|
|
trace.oa
|
|
} else {
|
|
trace.sa
|
|
};
|
|
let choice = Choice {
|
|
id: seed.id,
|
|
label: format!("{} · reserved @{}", seed.name, seed.alias),
|
|
};
|
|
self.seeds.insert(seed.id, seed);
|
|
if direction.target_layer() == Layer::Oa {
|
|
self.oa = Some(choice)
|
|
} else {
|
|
self.sa = Some(choice)
|
|
};
|
|
}
|
|
Event::Input(facet, value) => {
|
|
let (_, which) = parts(&facet).ok_or("Choose artifact metadata.")?;
|
|
if !compatible(which, &value) || !self.drafts.contains_key(&facet) {
|
|
return Err(
|
|
"Choose Set final value for this exact artifact metadata first.".into(),
|
|
);
|
|
}
|
|
if which == ArtifactFacet::Description
|
|
&& let Raw::Text(text) = &value
|
|
{
|
|
self.descriptions
|
|
.insert(facet.clone(), text_editor::Content::with_text(text));
|
|
}
|
|
self.drafts.insert(facet, value);
|
|
}
|
|
Event::Description(facet, action) => {
|
|
let editor = self
|
|
.descriptions
|
|
.get_mut(&facet)
|
|
.ok_or("Choose the artifact description draft first.")?;
|
|
editor.perform(action);
|
|
self.drafts.insert(facet, Raw::Text(editor.text()));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
pub fn editable(
|
|
&self,
|
|
p: &Project,
|
|
trace: &Trace,
|
|
facet: &FacetSelector,
|
|
layer: Layer,
|
|
) -> bool {
|
|
let Some((keys, which)) = parts(facet) else {
|
|
return false;
|
|
};
|
|
key(keys, layer).is_some()
|
|
&& capture_facet(p, trace.oa, trace.sa, facet).is_ok()
|
|
&& (which == ArtifactFacet::Presence
|
|
|| key(keys, layer).is_some_and(|id| {
|
|
p.artifact_links.contains_key(&id) || self.seeds.contains_key(&id)
|
|
}))
|
|
}
|
|
pub fn propose(
|
|
&self,
|
|
p: &Project,
|
|
trace: &Trace,
|
|
direction: TransitionDirection,
|
|
references: &BTreeMap<Id, Id>,
|
|
decisions: &BTreeMap<FacetSelector, SemanticDecision>,
|
|
) -> Result<Proposal, String> {
|
|
let mut result = Proposal {
|
|
slots: vec![],
|
|
creations: vec![],
|
|
candidates: BTreeSet::new(),
|
|
};
|
|
let mut pairs: BTreeMap<Id, Vec<(&FacetSelector, SemanticDecision)>> = BTreeMap::new();
|
|
for (facet, decision) in decisions {
|
|
if let Some((keys, _)) = parts(facet) {
|
|
let id = key(keys, direction.target_layer())
|
|
.ok_or("Choose a destination artifact identity.")?;
|
|
pairs.entry(id).or_default().push((facet, *decision));
|
|
}
|
|
}
|
|
for (id, selected) in pairs {
|
|
let original = p.artifact_links.get(&id);
|
|
let mut record = original.cloned();
|
|
let mut deletion_origin = None;
|
|
let owner = if direction.target_layer() == Layer::Oa {
|
|
trace.oa
|
|
} else {
|
|
trace.sa
|
|
};
|
|
for (facet, decision) in &selected {
|
|
let (keys, which) = parts(facet).expect("Artifact");
|
|
if which != ArtifactFacet::Presence {
|
|
continue;
|
|
}
|
|
if *decision == SemanticDecision::Candidate {
|
|
return Err("Artifact presence uses Keep or Take; deletion requires explicit source-owner absence.".into());
|
|
}
|
|
if *decision == SemanticDecision::TakeSource
|
|
&& key(keys, direction.source_layer()).is_some()
|
|
{
|
|
let fact = capture_facet(p, trace.oa, trace.sa, facet)?;
|
|
let present = scalar::value(&fact, direction.source_layer())
|
|
.and_then(|value| value.as_bool())
|
|
.ok_or("Source artifact attachment unavailable.")?;
|
|
if !present {
|
|
record = None;
|
|
deletion_origin = Some((*facet).clone());
|
|
} else if record.is_none() {
|
|
record=Some(self.seeds.get(&id).cloned().ok_or("Reserve a complete destination artifact counterpart before taking its presence.")?);
|
|
} else if record.as_ref().is_some_and(|a| a.owner != owner) {
|
|
return Err("Take presence cannot move an artifact from another owner; review its Owner from the initially attached trace.".into());
|
|
}
|
|
}
|
|
}
|
|
for (facet, decision) in &selected {
|
|
let (keys, which) = parts(facet).expect("Artifact");
|
|
if which == ArtifactFacet::Presence {
|
|
continue;
|
|
}
|
|
for (layer, side_owner) in [(Layer::Oa, trace.oa), (Layer::Sa, trace.sa)] {
|
|
if key(keys, layer)
|
|
.and_then(|id| p.artifact_links.get(&id))
|
|
.is_some_and(|a| a.owner != side_owner)
|
|
{
|
|
return Err("Artifact metadata must be reviewed from its initially attached owner trace.".into());
|
|
}
|
|
}
|
|
if *decision == SemanticDecision::KeepTarget {
|
|
continue;
|
|
}
|
|
let value = if *decision == SemanticDecision::Candidate {
|
|
self.drafts
|
|
.get(*facet)
|
|
.cloned()
|
|
.ok_or("Choose an explicit final metadata value.")?
|
|
} else {
|
|
if key(keys, direction.source_layer()).is_none() {
|
|
continue;
|
|
}
|
|
let source=key(keys,direction.source_layer()).and_then(|id|p.artifact_links.get(&id)).ok_or("Source artifact metadata is absent; keep or set the target metadata explicitly.")?;
|
|
let mut value = raw(source, which).expect("Metadata");
|
|
if let Raw::Owner(ref mut owner) = value {
|
|
let from = source.owner;
|
|
let target = if direction == TransitionDirection::OaToSa {
|
|
references.get(&from).copied()
|
|
} else {
|
|
let ids: Vec<_> = references
|
|
.iter()
|
|
.filter(|(_, sa)| **sa == from)
|
|
.map(|(oa, _)| *oa)
|
|
.collect();
|
|
(ids.len() == 1).then(|| ids[0])
|
|
}
|
|
.ok_or("Map the source artifact owner explicitly.")?;
|
|
*owner = target.to_string();
|
|
}
|
|
value
|
|
};
|
|
write(
|
|
record
|
|
.as_mut()
|
|
.ok_or("This metadata decision conflicts with absent artifact presence.")?,
|
|
which,
|
|
&value,
|
|
)?;
|
|
if *decision == SemanticDecision::Candidate {
|
|
result.candidates.insert(id);
|
|
}
|
|
}
|
|
for (facet, decision) in &selected {
|
|
let (_, which) = parts(facet).expect("Artifact");
|
|
result.slots.push((
|
|
slot(id, which),
|
|
observation(
|
|
if *decision == SemanticDecision::KeepTarget {
|
|
original
|
|
} else {
|
|
record.as_ref()
|
|
},
|
|
which,
|
|
),
|
|
(*facet).clone(),
|
|
));
|
|
}
|
|
if original.is_some() && record.is_none() {
|
|
for which in METADATA {
|
|
result.slots.push((
|
|
slot(id, which),
|
|
observation(None, which),
|
|
deletion_origin
|
|
.clone()
|
|
.expect("Absent record has an explicit Presence decision"),
|
|
));
|
|
}
|
|
}
|
|
if original.is_none()
|
|
&& let Some(record) = record
|
|
{
|
|
result.candidates.insert(id);
|
|
result.creations.push(record);
|
|
}
|
|
}
|
|
Ok(result)
|
|
}
|
|
pub fn recovery_snapshot(&self) -> Saved {
|
|
Saved {
|
|
oa: self.oa.as_ref().map(|c| c.id),
|
|
sa: self.sa.as_ref().map(|c| c.id),
|
|
seeds: self.seeds.values().cloned().collect(),
|
|
drafts: self
|
|
.drafts
|
|
.iter()
|
|
.map(|(facet, value)| (facet.clone(), value.clone()))
|
|
.collect(),
|
|
}
|
|
}
|
|
pub fn from_recovery(
|
|
p: &Project,
|
|
trace: &Trace,
|
|
direction: TransitionDirection,
|
|
saved: Saved,
|
|
) -> Result<Self, String> {
|
|
if saved.seeds.len().saturating_add(saved.drafts.len())
|
|
> crate::source_workspace::LIMITS.model.max_records
|
|
{
|
|
return Err("Artifact recovery exceeds supported limits.".into());
|
|
}
|
|
let mut editor = Self::default();
|
|
let owner = if direction.target_layer() == Layer::Oa {
|
|
trace.oa
|
|
} else {
|
|
trace.sa
|
|
};
|
|
let mut aliases = BTreeSet::new();
|
|
for seed in saved.seeds {
|
|
if p.contains(seed.id)
|
|
|| seed.owner != owner
|
|
|| seed.alias != format!("artifact_{}", seed.id.simple())
|
|
|| !aliases.insert(seed.alias.clone())
|
|
|| editor.seeds.insert(seed.id, seed).is_some()
|
|
{
|
|
return Err("Recovered reserved artifact identity, owner or alias is invalid or duplicated.".into());
|
|
}
|
|
}
|
|
for (facet, value) in saved.drafts {
|
|
let (keys, which) =
|
|
parts(&facet).ok_or("Recovered artifact draft has a different facet family.")?;
|
|
if !compatible(which, &value) {
|
|
return Err("Recovered artifact draft type disagrees with its facet.".into());
|
|
}
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
if key(keys, layer)
|
|
.and_then(|id| p.layer_of(id))
|
|
.is_some_and(|actual| actual != layer)
|
|
{
|
|
return Err("Recovered artifact pair has a wrong-layer identity.".into());
|
|
}
|
|
}
|
|
let id = key(keys, direction.target_layer())
|
|
.ok_or("Recovered artifact draft has no destination identity.")?;
|
|
if !p.artifact_links.contains_key(&id) && !editor.seeds.contains_key(&id) {
|
|
return Err(
|
|
"Recovered artifact draft has no existing or reserved destination record."
|
|
.into(),
|
|
);
|
|
}
|
|
if let (ArtifactFacet::Description, Raw::Text(text)) = (which, &value) {
|
|
editor
|
|
.descriptions
|
|
.insert(facet.clone(), text_editor::Content::with_text(text));
|
|
}
|
|
if editor.drafts.insert(facet, value).is_some() {
|
|
return Err("Recovered artifact draft address is duplicated.".into());
|
|
}
|
|
}
|
|
editor.oa = saved.oa.map(|id| editor.choice(p, id));
|
|
editor.sa = saved.sa.map(|id| editor.choice(p, id));
|
|
Ok(editor)
|
|
}
|
|
}
|