Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user