Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
729 lines
28 KiB
Rust
729 lines
28 KiB
Rust
//! Recover authored intent against the exact accepted model; recompute every plan.
|
|
use super::*;
|
|
use crate::forms::RecoveryDraft;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(super) struct Saved {
|
|
version: u32,
|
|
base_identity: Id,
|
|
base_fingerprint: String,
|
|
anchor: Id,
|
|
trace: Option<Id>,
|
|
direction: TransitionDirection,
|
|
selected: Option<FacetSelector>,
|
|
decisions: Vec<(FacetSelector, SemanticDecision)>,
|
|
references: BTreeMap<Id, Id>,
|
|
resets: BTreeSet<FacetSelector>,
|
|
staged: BTreeSet<FacetSelector>,
|
|
presence: Vec<(FacetSelector, bool)>,
|
|
member_family: keyed::Family,
|
|
member_oa: Option<Id>,
|
|
member_sa: Option<Id>,
|
|
literal: String,
|
|
literal_oa: bool,
|
|
literal_sa: bool,
|
|
annotation_oa: Option<AnnotationKey>,
|
|
annotation_sa: Option<AnnotationKey>,
|
|
draft: Option<RecoveryDraft>,
|
|
group_drafts: Vec<(FacetSelector, RecoveryDraft)>,
|
|
rows: Vec<(FacetSelector, String, Option<Id>)>,
|
|
#[serde(default)]
|
|
orders: Vec<(OrderedCollection, Vec<Id>)>,
|
|
#[serde(default)]
|
|
attachments: attachments::Saved,
|
|
#[serde(default)]
|
|
artifacts: artifacts::Saved,
|
|
#[serde(default)]
|
|
operation_omission: Option<String>,
|
|
reference_oa: Option<Id>,
|
|
reference_sa: Option<Id>,
|
|
reference_search: String,
|
|
accepted_impacts: Vec<SemanticImpact>,
|
|
}
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct RowPayload {
|
|
anchor: Id,
|
|
trace: Option<Id>,
|
|
direction: TransitionDirection,
|
|
selected: Option<FacetSelector>,
|
|
decisions: Vec<(FacetSelector, SemanticDecision)>,
|
|
resets: BTreeSet<FacetSelector>,
|
|
staged: BTreeSet<FacetSelector>,
|
|
presence: Vec<(FacetSelector, bool)>,
|
|
member_family: keyed::Family,
|
|
member_oa: Option<Id>,
|
|
member_sa: Option<Id>,
|
|
literal: String,
|
|
literal_oa: bool,
|
|
literal_sa: bool,
|
|
annotation_oa: Option<AnnotationKey>,
|
|
annotation_sa: Option<AnnotationKey>,
|
|
draft: Option<RecoveryDraft>,
|
|
group_drafts: Vec<(FacetSelector, RecoveryDraft)>,
|
|
rows: Vec<(FacetSelector, String, Option<Id>)>,
|
|
#[serde(default)]
|
|
orders: Vec<(OrderedCollection, Vec<Id>)>,
|
|
#[serde(default)]
|
|
attachments: attachments::Saved,
|
|
#[serde(default)]
|
|
artifacts: artifacts::Saved,
|
|
#[serde(default)]
|
|
operation_omission: Option<String>,
|
|
reference_oa: Option<Id>,
|
|
reference_sa: Option<Id>,
|
|
reference_search: String,
|
|
}
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct SavedRow {
|
|
address: RowAddress,
|
|
included: bool,
|
|
draft: RowPayload,
|
|
}
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct SavedBatch {
|
|
version: u32,
|
|
base_identity: Id,
|
|
base_fingerprint: String,
|
|
direction: TransitionDirection,
|
|
references: BTreeMap<Id, Id>,
|
|
accepted_impacts: Vec<SemanticImpact>,
|
|
active: Option<RowAddress>,
|
|
rows: Vec<SavedRow>,
|
|
pending: Option<RowPayload>,
|
|
add_trace: Option<Id>,
|
|
}
|
|
impl RowPayload {
|
|
fn from_row(row: &ReviewRow) -> Result<Self, String> {
|
|
let saved: Saved =
|
|
serde_json::from_value(row.recovery_snapshot()?).map_err(|e| e.to_string())?;
|
|
Ok(Self {
|
|
anchor: saved.anchor,
|
|
trace: saved.trace,
|
|
direction: saved.direction,
|
|
selected: saved.selected,
|
|
decisions: saved.decisions,
|
|
resets: saved.resets,
|
|
staged: saved.staged,
|
|
presence: saved.presence,
|
|
member_family: saved.member_family,
|
|
member_oa: saved.member_oa,
|
|
member_sa: saved.member_sa,
|
|
literal: saved.literal,
|
|
literal_oa: saved.literal_oa,
|
|
literal_sa: saved.literal_sa,
|
|
annotation_oa: saved.annotation_oa,
|
|
annotation_sa: saved.annotation_sa,
|
|
draft: saved.draft,
|
|
group_drafts: saved.group_drafts,
|
|
rows: saved.rows,
|
|
orders: saved.orders,
|
|
attachments: saved.attachments,
|
|
artifacts: saved.artifacts,
|
|
operation_omission: saved.operation_omission,
|
|
reference_oa: saved.reference_oa,
|
|
reference_sa: saved.reference_sa,
|
|
reference_search: saved.reference_search,
|
|
})
|
|
}
|
|
fn restore(
|
|
self,
|
|
project: &Project,
|
|
references: &BTreeMap<Id, Id>,
|
|
) -> Result<ReviewRow, String> {
|
|
ReviewRow::restore_saved(
|
|
project,
|
|
Saved {
|
|
version: 1,
|
|
base_identity: project.id,
|
|
base_fingerprint: String::new(),
|
|
references: references.clone(),
|
|
accepted_impacts: vec![],
|
|
anchor: self.anchor,
|
|
trace: self.trace,
|
|
direction: self.direction,
|
|
selected: self.selected,
|
|
decisions: self.decisions,
|
|
resets: self.resets,
|
|
staged: self.staged,
|
|
presence: self.presence,
|
|
member_family: self.member_family,
|
|
member_oa: self.member_oa,
|
|
member_sa: self.member_sa,
|
|
literal: self.literal,
|
|
literal_oa: self.literal_oa,
|
|
literal_sa: self.literal_sa,
|
|
annotation_oa: self.annotation_oa,
|
|
annotation_sa: self.annotation_sa,
|
|
draft: self.draft,
|
|
group_drafts: self.group_drafts,
|
|
rows: self.rows,
|
|
orders: self.orders,
|
|
attachments: self.attachments,
|
|
artifacts: self.artifacts,
|
|
operation_omission: self.operation_omission,
|
|
reference_oa: self.reference_oa,
|
|
reference_sa: self.reference_sa,
|
|
reference_search: self.reference_search,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
fn unique<K: Ord, V>(values: Vec<(K, V)>) -> Result<BTreeMap<K, V>, String> {
|
|
let mut map = BTreeMap::new();
|
|
for (key, value) in values {
|
|
if map.insert(key, value).is_some() {
|
|
return Err("Recovery contains a duplicate semantic draft address.".into());
|
|
}
|
|
}
|
|
Ok(map)
|
|
}
|
|
|
|
impl ReviewRow {
|
|
pub fn recovery_snapshot(&self) -> Result<Value, String> {
|
|
let key = |address: &keyed_rows::AddressDraft| {
|
|
address.selected.as_ref().map(|k| AnnotationKey {
|
|
path: k.path.clone(),
|
|
field: k.field,
|
|
})
|
|
};
|
|
serde_json::to_value(Saved {
|
|
version: 1,
|
|
base_identity: self.base_identity,
|
|
base_fingerprint: self.base_fingerprint.clone(),
|
|
anchor: self.anchor,
|
|
trace: self.trace,
|
|
direction: self.direction,
|
|
selected: self.selected.clone(),
|
|
decisions: self
|
|
.decisions
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), *value))
|
|
.collect(),
|
|
references: self.references.clone(),
|
|
resets: self.resets.clone(),
|
|
staged: self.staged.clone(),
|
|
presence: self
|
|
.presence
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), *value))
|
|
.collect(),
|
|
member_family: self.member_family,
|
|
member_oa: self.member_oa.as_ref().map(|c| c.id),
|
|
member_sa: self.member_sa.as_ref().map(|c| c.id),
|
|
literal: self.literal.clone(),
|
|
literal_oa: self.literal_oa,
|
|
literal_sa: self.literal_sa,
|
|
annotation_oa: key(&self.annotation_oa),
|
|
annotation_sa: key(&self.annotation_sa),
|
|
draft: self.draft.as_ref().map(Draft::recovery_snapshot),
|
|
group_drafts: self
|
|
.group_drafts
|
|
.iter()
|
|
.map(|(key, draft)| (key.clone(), draft.recovery_snapshot()))
|
|
.collect(),
|
|
rows: self
|
|
.row_drafts
|
|
.iter()
|
|
.map(|(key, draft)| {
|
|
(
|
|
key.clone(),
|
|
draft.expression.clone(),
|
|
draft.local.as_ref().map(|c| c.id),
|
|
)
|
|
})
|
|
.collect(),
|
|
attachments: self.attachments.recovery_snapshot(),
|
|
artifacts: self.artifacts.recovery_snapshot(),
|
|
operation_omission: self.operation_omission.clone(),
|
|
reference_oa: self.reference_oa.as_ref().map(|c| c.id),
|
|
orders: self
|
|
.order_drafts
|
|
.iter()
|
|
.map(|(key, value)| (*key, value.clone()))
|
|
.collect(),
|
|
reference_sa: self.reference_sa.as_ref().map(|c| c.id),
|
|
reference_search: self.reference_search.clone(),
|
|
accepted_impacts: self.accepted_impacts.clone(),
|
|
})
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
pub fn from_recovery(project: &Project, value: Value) -> Result<Self, String> {
|
|
let saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?;
|
|
if saved.version != 1
|
|
|| saved.base_identity != project.id
|
|
|| saved.base_fingerprint != project_fingerprint(project)
|
|
{
|
|
return Err(
|
|
"Semantic review recovery does not match the accepted model identity and revision."
|
|
.into(),
|
|
);
|
|
}
|
|
Self::restore_saved(project, saved)
|
|
}
|
|
|
|
fn restore_saved(project: &Project, saved: Saved) -> Result<Self, String> {
|
|
let count = [
|
|
saved.decisions.len(),
|
|
saved.references.len(),
|
|
saved.resets.len(),
|
|
saved.staged.len(),
|
|
saved.presence.len(),
|
|
saved.rows.len(),
|
|
saved
|
|
.orders
|
|
.iter()
|
|
.fold(0usize, |total, (_, rows)| total.saturating_add(rows.len())),
|
|
saved.accepted_impacts.len(),
|
|
]
|
|
.into_iter()
|
|
.fold(0usize, usize::saturating_add);
|
|
if count > crate::source_workspace::LIMITS.model.max_records
|
|
|| saved.group_drafts.len() > constructors::selectors().len()
|
|
{
|
|
return Err("Semantic recovery exceeds the supported draft limits.".into());
|
|
}
|
|
let mut state = Self::new(project, saved.anchor)?;
|
|
if saved.trace.is_some_and(|id| {
|
|
!project
|
|
.traces
|
|
.get(&id)
|
|
.is_some_and(|trace| trace.oa == saved.anchor || trace.sa == saved.anchor)
|
|
}) {
|
|
return Err("Semantic recovery trace and selected object disagree.".into());
|
|
}
|
|
state.base_fingerprint = project_fingerprint(project);
|
|
state.trace = saved.trace;
|
|
state.direction = saved.direction;
|
|
state.load_trace(project);
|
|
state.selected = saved.selected;
|
|
state.decisions = unique(saved.decisions)?;
|
|
state.references = saved.references;
|
|
state.resets = saved.resets;
|
|
state.staged = saved.staged;
|
|
state.presence = unique(saved.presence)?;
|
|
state.member_family = saved.member_family;
|
|
state.member_oa = saved.member_oa.map(|id| view::choice(project, id));
|
|
state.member_sa = saved.member_sa.map(|id| view::choice(project, id));
|
|
state.literal = saved.literal;
|
|
state.literal_oa = saved.literal_oa;
|
|
state.literal_sa = saved.literal_sa;
|
|
let address = |key: Option<AnnotationKey>| keyed_rows::AddressDraft {
|
|
selected: key.map(|key| crate::data_values::FieldPathKey {
|
|
path: key.path,
|
|
field: key.field,
|
|
}),
|
|
..Default::default()
|
|
};
|
|
state.annotation_oa = address(saved.annotation_oa);
|
|
state.annotation_sa = address(saved.annotation_sa);
|
|
let target = state.target(project);
|
|
state.draft = saved
|
|
.draft
|
|
.map(|draft| Draft::from_recovery(project, draft))
|
|
.transpose()?;
|
|
if state
|
|
.draft
|
|
.as_ref()
|
|
.is_some_and(|draft| Some(draft.original) != target)
|
|
{
|
|
return Err("Recovered semantic draft belongs to another target.".into());
|
|
}
|
|
state.group_drafts = unique(
|
|
saved
|
|
.group_drafts
|
|
.into_iter()
|
|
.map(|(facet, draft)| {
|
|
if !constructors::selectors().contains(&facet) {
|
|
return Err("Recovered constructor draft has an unknown scope.".to_owned());
|
|
}
|
|
let mut draft = Draft::from_recovery(project, draft)?;
|
|
if Some(draft.original) != target {
|
|
return Err("Recovered constructor draft belongs to another target.".into());
|
|
}
|
|
draft.semantic_groups = true;
|
|
Ok((facet, draft))
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?,
|
|
)?;
|
|
state.row_drafts = unique(
|
|
saved
|
|
.rows
|
|
.into_iter()
|
|
.map(|(facet, expression, local)| {
|
|
if !keyed_rows::supported(&facet) {
|
|
return Err("Recovered row draft has an unknown scope.".to_owned());
|
|
}
|
|
Ok((
|
|
facet,
|
|
keyed_rows::RowDraft {
|
|
editor: text_editor::Content::with_text(&expression),
|
|
expression,
|
|
local: local.map(|id| view::choice(project, id)),
|
|
},
|
|
))
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?,
|
|
)?;
|
|
state.reference_oa = saved.reference_oa.map(|id| view::choice(project, id));
|
|
state.order_drafts = unique(saved.orders)?;
|
|
state.attachments = attachments::Editor::from_recovery(
|
|
project,
|
|
state.direction.target_layer(),
|
|
saved.attachments,
|
|
)?;
|
|
if let Some(trace) = state.trace.and_then(|id| project.traces.get(&id)) {
|
|
state.artifacts =
|
|
artifacts::Editor::from_recovery(project, trace, state.direction, saved.artifacts)?;
|
|
} else if !saved.artifacts.is_empty() {
|
|
return Err("Recovered artifact drafts need an exact owner trace.".into());
|
|
}
|
|
state.reference_sa = saved.reference_sa.map(|id| view::choice(project, id));
|
|
state.operation_omission = saved.operation_omission;
|
|
state.reference_search = saved.reference_search;
|
|
state.accepted_impacts = saved.accepted_impacts;
|
|
// Invalid numbers and incomplete mappings are recoverable authored input.
|
|
// Refresh reports their errors instead of dropping them or applying a plan.
|
|
state.refresh(project);
|
|
Ok(state)
|
|
}
|
|
}
|
|
|
|
fn batch_limits(value: &Value) -> Result<(), String> {
|
|
let mut pending = vec![value];
|
|
let mut count = 0usize;
|
|
let mut bytes = 0usize;
|
|
while let Some(value) = pending.pop() {
|
|
count = count.saturating_add(1);
|
|
match value {
|
|
Value::String(text) => bytes = bytes.saturating_add(text.len()),
|
|
Value::Array(values) => pending.extend(values),
|
|
Value::Object(values) => {
|
|
bytes = values
|
|
.keys()
|
|
.fold(bytes, |total, key| total.saturating_add(key.len()));
|
|
pending.extend(values.values());
|
|
}
|
|
_ => {}
|
|
}
|
|
if count > crate::source_workspace::LIMITS.model.max_records
|
|
|| bytes > crate::source_workspace::LIMITS.model.max_source_bytes
|
|
{
|
|
return Err("Semantic recovery exceeds the aggregate draft limits.".into());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl State {
|
|
/// A source-5/6 upgrade may reconstruct the same accepted records with a new
|
|
/// schema fingerprint. The caller supplies the independently verified legacy
|
|
/// base; every saved row is still decoded and replanned through strict restore.
|
|
pub(crate) fn from_recovery_after_source5_upgrade(
|
|
project: &Project,
|
|
legacy_base: &str,
|
|
value: Value,
|
|
) -> Result<Self, String> {
|
|
batch_limits(&value)?;
|
|
crate::recovery::validate_pre7_intent(&value)?;
|
|
let value = match value.get("version").and_then(Value::as_u64) {
|
|
Some(1) => {
|
|
let mut saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?;
|
|
if saved.base_identity != project.id || saved.base_fingerprint != legacy_base {
|
|
return Err(
|
|
"Semantic legacy recovery does not match the verified legacy base.".into(),
|
|
);
|
|
}
|
|
saved.base_fingerprint = project_fingerprint(project);
|
|
saved.accepted_impacts.clear();
|
|
serde_json::to_value(saved).map_err(|e| e.to_string())?
|
|
}
|
|
Some(2) => {
|
|
let mut saved: SavedBatch =
|
|
serde_json::from_value(value).map_err(|e| e.to_string())?;
|
|
if saved.base_identity != project.id || saved.base_fingerprint != legacy_base {
|
|
return Err(
|
|
"Semantic legacy recovery does not match the verified legacy base.".into(),
|
|
);
|
|
}
|
|
saved.base_fingerprint = project_fingerprint(project);
|
|
saved.accepted_impacts.clear();
|
|
serde_json::to_value(saved).map_err(|e| e.to_string())?
|
|
}
|
|
_ => return Err("Unsupported semantic recovery version.".into()),
|
|
};
|
|
Self::from_recovery(project, value)
|
|
}
|
|
|
|
pub fn recovery_snapshot(&self) -> Result<Value, String> {
|
|
let rows = self
|
|
.order
|
|
.iter()
|
|
.map(|address| {
|
|
Ok(SavedRow {
|
|
address: *address,
|
|
included: self.included.contains(address),
|
|
draft: RowPayload::from_row(
|
|
self.row(*address)
|
|
.ok_or("Retained review row is missing.")?,
|
|
)?,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
if rows.len() > batch::MAX_REVIEW_ROWS {
|
|
return Err("Semantic recovery exceeds the supported row limit.".into());
|
|
}
|
|
let saved = SavedBatch {
|
|
version: 2,
|
|
base_identity: self.base_identity,
|
|
base_fingerprint: self.base_fingerprint.clone(),
|
|
direction: self.direction,
|
|
references: self.references.clone(),
|
|
accepted_impacts: self.accepted_impacts.clone(),
|
|
active: self.active_address,
|
|
rows,
|
|
pending: if self.active_address.is_none() {
|
|
Some(RowPayload::from_row(&self.active)?)
|
|
} else {
|
|
None
|
|
},
|
|
add_trace: self.add_trace.as_ref().map(|choice| choice.id),
|
|
};
|
|
let value = serde_json::to_value(saved).map_err(|error| error.to_string())?;
|
|
batch_limits(&value)?;
|
|
Ok(value)
|
|
}
|
|
pub fn from_recovery(project: &Project, value: Value) -> Result<Self, String> {
|
|
batch_limits(&value)?;
|
|
if value.get("version").and_then(Value::as_u64) == Some(1) {
|
|
let active = ReviewRow::from_recovery(project, value)?;
|
|
let address = active
|
|
.trace
|
|
.and_then(|id| project.traces.get(&id))
|
|
.map(|trace| RowAddress::from_trace(trace, active.direction));
|
|
let mut state = Self {
|
|
base: std::sync::Arc::new(project.clone()),
|
|
base_identity: project.id,
|
|
base_fingerprint: project_fingerprint(project),
|
|
direction: active.direction,
|
|
references: active.references.clone(),
|
|
accepted_impacts: active.accepted_impacts.clone(),
|
|
plan: None,
|
|
proposed: None,
|
|
error: None,
|
|
active,
|
|
active_address: address,
|
|
bank: BTreeMap::new(),
|
|
order: address.into_iter().collect(),
|
|
included: address.into_iter().collect(),
|
|
add_trace: None,
|
|
notice: None,
|
|
};
|
|
state.refresh(project);
|
|
return Ok(state);
|
|
}
|
|
let saved: SavedBatch = serde_json::from_value(value).map_err(|error| error.to_string())?;
|
|
if saved.version != 2
|
|
|| saved.base_identity != project.id
|
|
|| saved.base_fingerprint != project_fingerprint(project)
|
|
{
|
|
return Err(
|
|
"Semantic review recovery does not match the accepted model identity and revision."
|
|
.into(),
|
|
);
|
|
}
|
|
if saved.rows.len() > batch::MAX_REVIEW_ROWS {
|
|
return Err("Semantic recovery exceeds the supported row limit.".into());
|
|
}
|
|
let mut bank = BTreeMap::new();
|
|
let mut order = vec![];
|
|
let mut included = BTreeSet::new();
|
|
let mut sources = BTreeSet::new();
|
|
for saved_row in saved.rows {
|
|
let address = saved_row.address;
|
|
let direction = address.direction(project)?;
|
|
let row = saved_row.draft.restore(project, &saved.references)?;
|
|
if row.direction != direction
|
|
|| row.trace != Some(address.trace)
|
|
|| row.target(project) != Some(address.target)
|
|
{
|
|
return Err(
|
|
"Recovered row draft does not match its stable source/target/trace address."
|
|
.into(),
|
|
);
|
|
}
|
|
if bank.insert(address, row).is_some() {
|
|
return Err("Recovery contains a duplicate semantic row address.".into());
|
|
}
|
|
order.push(address);
|
|
if saved_row.included {
|
|
if !sources.insert(address.source) {
|
|
return Err(
|
|
"Recovery contains multiple included histories for one source.".into(),
|
|
);
|
|
}
|
|
included.insert(address);
|
|
}
|
|
}
|
|
let active = match (saved.active, saved.pending) {
|
|
(Some(address), None) => {
|
|
if address.direction(project)? != saved.direction {
|
|
return Err("Recovery active row has a different direction.".into());
|
|
}
|
|
bank.remove(&address)
|
|
.ok_or("Recovery active review row is absent.")?
|
|
}
|
|
(None, Some(pending)) if bank.is_empty() => {
|
|
let row = pending.restore(project, &saved.references)?;
|
|
if row.trace.is_some() || row.direction != saved.direction {
|
|
return Err("Recovery pending row is not an unresolved trace selection.".into());
|
|
}
|
|
row
|
|
}
|
|
_ => return Err("Recovery has an invalid active/pending review row.".into()),
|
|
};
|
|
if saved
|
|
.add_trace
|
|
.is_some_and(|id| !project.traces.contains_key(&id))
|
|
{
|
|
return Err("Recovery add-row selection is not an existing trace.".into());
|
|
}
|
|
let mut state = Self {
|
|
base: std::sync::Arc::new(project.clone()),
|
|
base_identity: saved.base_identity,
|
|
base_fingerprint: saved.base_fingerprint,
|
|
direction: saved.direction,
|
|
references: saved.references,
|
|
accepted_impacts: saved.accepted_impacts,
|
|
plan: None,
|
|
proposed: None,
|
|
error: None,
|
|
active,
|
|
active_address: saved.active,
|
|
bank,
|
|
order,
|
|
included,
|
|
add_trace: saved.add_trace.map(|id| view::choice(project, id)),
|
|
notice: None,
|
|
};
|
|
// Recompose all included rows once. Excluded invalid raw drafts remain recoverable.
|
|
state.refresh(project);
|
|
Ok(state)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn version_one_without_orders_restores_raw_intent_and_replans_without_execution() {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::DataType, Layer::Oa, "Operational", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::DataType, Layer::Sa, "System", None)
|
|
.unwrap();
|
|
for id in [oa, sa] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer {
|
|
minimum: Some(0),
|
|
maximum: Some(100),
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
super::super::tests::add_trace(&mut p, oa, sa, "Version one review");
|
|
let accepted = p.clone();
|
|
let facet = FacetSelector::Data(DataFacet::IntegerMinimum);
|
|
let path = "/body/Sa/Shared/DataType/definition/Integer/minimum";
|
|
for (raw, valid) in [("7", true), ("-", false)] {
|
|
let mut state = State::new(&p, oa).unwrap();
|
|
state.update(&p, RowEvent::Select(facet.clone()));
|
|
state.update(
|
|
&p,
|
|
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
state.update(&p, RowEvent::Input(path.into(), raw.into()));
|
|
assert_eq!(state.can_apply(), valid);
|
|
let current = state.active.recovery_snapshot().unwrap();
|
|
let mut old = current.clone();
|
|
assert_eq!(old["version"], 1);
|
|
assert_eq!(
|
|
old.as_object_mut().unwrap().remove("orders"),
|
|
Some(serde_json::json!([]))
|
|
);
|
|
let restored = State::from_recovery(&p, old.clone()).unwrap();
|
|
assert!(restored.order_drafts.is_empty());
|
|
assert_eq!(restored.draft.as_ref().unwrap().inputs[path].raw, raw);
|
|
assert_eq!(restored.can_apply(), valid);
|
|
assert_eq!(restored.active.recovery_snapshot().unwrap(), current);
|
|
assert_eq!(
|
|
p, accepted,
|
|
"Recovery prepares a preview, never an accepted edit"
|
|
);
|
|
let mut changed = p.clone();
|
|
changed.name.push_str(" changed accepted model");
|
|
assert!(State::from_recovery(&changed, old).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn raw_scalar_and_closed_constructor_drafts_recover_against_exact_model_only() {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::DataType, Layer::Oa, "Operational", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::DataType, Layer::Sa, "System", None)
|
|
.unwrap();
|
|
for id in [oa, sa] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer {
|
|
minimum: Some(0),
|
|
maximum: Some(100),
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
super::super::tests::add_trace(&mut p, oa, sa, "Recover review");
|
|
let mut state = State::new(&p, oa).unwrap();
|
|
let facet = FacetSelector::Data(DataFacet::IntegerMinimum);
|
|
state.update(&p, RowEvent::Select(facet.clone()));
|
|
state.update(
|
|
&p,
|
|
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
let path = "/body/Sa/Shared/DataType/definition/Integer/minimum";
|
|
state.update(&p, RowEvent::Input(path.into(), "-".into()));
|
|
assert!(!state.can_apply());
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
let restored = State::from_recovery(&p, saved.clone()).unwrap();
|
|
assert_eq!(restored.draft.as_ref().unwrap().inputs[path].raw, "-");
|
|
assert_eq!(restored.decisions, state.decisions);
|
|
assert!(!restored.can_apply());
|
|
assert!(restored.plan.is_none());
|
|
assert!(
|
|
restored
|
|
.group_drafts
|
|
.values()
|
|
.all(|draft| draft.semantic_groups)
|
|
);
|
|
p.name.push_str(" changed");
|
|
assert!(
|
|
State::from_recovery(&p, saved)
|
|
.err()
|
|
.unwrap()
|
|
.contains("identity and revision")
|
|
);
|
|
}
|
|
}
|