Review closed semantic constructor groups with retained typed drafts
This commit is contained in:
@@ -8,11 +8,14 @@ use oa_core::{transitions::*, *};
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
mod constructors;
|
||||
mod message_kind;
|
||||
mod scalar;
|
||||
mod view;
|
||||
pub(crate) use view::summary;
|
||||
#[cfg(test)]
|
||||
mod constructor_tests;
|
||||
#[cfg(test)]
|
||||
mod message_kind_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -57,9 +60,9 @@ pub struct State {
|
||||
pub decisions: BTreeMap<FacetSelector, SemanticDecision>,
|
||||
pub references: BTreeMap<Id, Id>,
|
||||
pub draft: Option<Draft>,
|
||||
// Group editing cannot reshape the independently authored TimerExpression
|
||||
// scalar draft. Both drafts survive decision changes until this trace closes.
|
||||
pub message_draft: Option<Draft>,
|
||||
// Each closed constructor has an independent draft, including nested kinds.
|
||||
// Scalar and sibling drafts survive decision and constructor changes.
|
||||
pub group_drafts: BTreeMap<FacetSelector, Draft>,
|
||||
pub plan: Option<SemanticTransitionPlan>,
|
||||
pub proposed: Option<Project>,
|
||||
pub error: Option<String>,
|
||||
@@ -102,7 +105,7 @@ impl State {
|
||||
decisions: BTreeMap::new(),
|
||||
references: BTreeMap::new(),
|
||||
draft: None,
|
||||
message_draft: None,
|
||||
group_drafts: BTreeMap::new(),
|
||||
plan: None,
|
||||
proposed: None,
|
||||
error: None,
|
||||
@@ -135,12 +138,19 @@ impl State {
|
||||
self.draft = self
|
||||
.target(project)
|
||||
.and_then(|id| Draft::from_project(project, id));
|
||||
self.message_draft = self
|
||||
.trace
|
||||
.and_then(|id| project.traces.get(&id))
|
||||
.filter(|trace| message_kind::available(project, trace))
|
||||
.and_then(|_| self.target(project))
|
||||
.and_then(|id| Draft::from_project(project, id));
|
||||
self.group_drafts.clear();
|
||||
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
|
||||
for facet in constructors::selectors() {
|
||||
if constructors::available(project, trace, &facet)
|
||||
&& let Some(mut draft) = self
|
||||
.target(project)
|
||||
.and_then(|id| Draft::from_project(project, id))
|
||||
{
|
||||
draft.semantic_groups = true;
|
||||
self.group_drafts.insert(facet, draft);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
|
||||
self.references.insert(trace.oa, trace.sa);
|
||||
}
|
||||
@@ -200,19 +210,9 @@ impl State {
|
||||
Event::Shape(path, value) => {
|
||||
self.edit(project, &path, |d| d.shape_scoped(&path, value))
|
||||
}
|
||||
Event::Variant(path, value) => self.edit(project, &path, |d| {
|
||||
if path.ends_with("/message_kind") {
|
||||
d.message_kind_variant(&path, value)
|
||||
} else if d
|
||||
.value
|
||||
.pointer(&path)
|
||||
.is_some_and(|v| crate::forms::same_variant(v, &value))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
d.shape_scoped(&path, value)
|
||||
}
|
||||
}),
|
||||
Event::Variant(path, value) => {
|
||||
self.edit(project, &path, |d| d.variant_scoped(&path, value))
|
||||
}
|
||||
Event::Array(path, index, direction) => {
|
||||
self.edit(project, &path, |d| d.array_scoped(&path, index, direction))
|
||||
}
|
||||
@@ -297,21 +297,23 @@ impl State {
|
||||
if self.decisions.get(facet) != Some(&SemanticDecision::Candidate) {
|
||||
return Err("Choose Set final value before editing this property.".into());
|
||||
}
|
||||
let root = self
|
||||
let roots = self
|
||||
.target(project)
|
||||
.and_then(|id| scalar::binding(project, id, facet))
|
||||
.ok_or("Selected scalar property is unavailable.")?;
|
||||
if path != root && !path.starts_with(&format!("{root}/")) {
|
||||
.and_then(|id| constructors::roots(project, id, facet))
|
||||
.ok_or("Selected property is unavailable.")?;
|
||||
if !roots
|
||||
.iter()
|
||||
.any(|root| path == root || path.starts_with(&format!("{root}/")))
|
||||
{
|
||||
return Err("Only the selected review property can change in this draft.".into());
|
||||
}
|
||||
let draft = if message_kind::handles(facet) {
|
||||
&mut self.message_draft
|
||||
let draft = if scalar::grouped(facet) {
|
||||
self.group_drafts.get_mut(facet)
|
||||
} else {
|
||||
&mut self.draft
|
||||
self.draft.as_mut()
|
||||
}
|
||||
.as_mut()
|
||||
.ok_or("Choose an existing target.")?;
|
||||
// The scoped view sends only descendants of the selected scalar property.
|
||||
// The scoped view sends only the selected property or closed group roots.
|
||||
// Protected identity and top-level body/perspective controls are never exposed.
|
||||
if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" {
|
||||
return Err("Semantic candidates retain identity, alias and perspective.".into());
|
||||
@@ -324,17 +326,16 @@ impl State {
|
||||
.and_then(|id| scalar::binding(project, id, facet))
|
||||
.is_some()
|
||||
&& (!scalar::grouped(facet)
|
||||
|| (message_kind::handles(facet)
|
||||
&& self.message_draft.is_some()
|
||||
|| (self.group_drafts.contains_key(facet)
|
||||
&& self
|
||||
.trace
|
||||
.and_then(|id| project.traces.get(&id))
|
||||
.is_some_and(|trace| message_kind::available(project, trace))))
|
||||
.is_some_and(|trace| constructors::available(project, trace, facet))))
|
||||
}
|
||||
|
||||
fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> {
|
||||
if message_kind::handles(facet) {
|
||||
self.message_draft.as_ref()
|
||||
if scalar::grouped(facet) {
|
||||
self.group_drafts.get(facet)
|
||||
} else {
|
||||
self.draft.as_ref()
|
||||
}
|
||||
@@ -418,7 +419,7 @@ impl State {
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// Compose selected scalar values for both the candidate and dependent form
|
||||
/// Compose selected values for both the candidate and dependent form
|
||||
/// controls. Incomplete inputs retain their stored shape only for controls;
|
||||
/// request construction always requires every selected input to parse.
|
||||
fn combined_value(&self, project: &Project, form_context: bool) -> Result<Value, String> {
|
||||
@@ -446,61 +447,75 @@ impl State {
|
||||
}
|
||||
}
|
||||
map.insert(source, target);
|
||||
let mut message_candidate = None;
|
||||
for (facet, decision) in &self.decisions {
|
||||
let original = final_value.clone();
|
||||
let mut group_candidates = vec![];
|
||||
// Outer groups precede nested groups and leaves. The core rejects actual
|
||||
// overlapping replacement scopes; this order also supports first-equal
|
||||
// discriminator acknowledgment together with explicit descendant values.
|
||||
let mut decisions: Vec<_> = self.decisions.iter().collect();
|
||||
decisions.sort_by_key(|(facet, _)| match facet {
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor) => 0,
|
||||
facet if scalar::grouped(facet) => 1,
|
||||
_ => 2,
|
||||
});
|
||||
for (facet, decision) in decisions {
|
||||
if *decision == SemanticDecision::KeepTarget {
|
||||
continue;
|
||||
}
|
||||
let path = scalar::binding(project, target, facet)
|
||||
let roots = constructors::roots(project, target, facet)
|
||||
.ok_or("Selected property is not applicable to the target.")?;
|
||||
let value = match decision {
|
||||
let supplied = match decision {
|
||||
SemanticDecision::KeepTarget => continue,
|
||||
SemanticDecision::Candidate => {
|
||||
let draft = self
|
||||
.candidate_draft(facet)
|
||||
.ok_or("Target draft is unavailable.")?;
|
||||
let value = match draft.materialize_scopes(std::slice::from_ref(&path)) {
|
||||
match draft.materialize_scopes(&roots) {
|
||||
Ok(value) => value,
|
||||
Err(_) if form_context => draft.value.clone(),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
value
|
||||
.pointer(&path)
|
||||
.cloned()
|
||||
.ok_or("Candidate property is no longer available.")?
|
||||
}
|
||||
}
|
||||
SemanticDecision::TakeSource if message_kind::handles(facet) => {
|
||||
// First equal kinds acknowledge only the discriminator.
|
||||
// An unselected Timer payload must stay at the destination.
|
||||
message_kind::source_value(
|
||||
project,
|
||||
if self.can_acknowledge_constructor(project, facet) {
|
||||
target
|
||||
} else {
|
||||
source
|
||||
},
|
||||
)?
|
||||
SemanticDecision::TakeSource if scalar::grouped(facet) => {
|
||||
if self.can_acknowledge_constructor(project, facet) {
|
||||
original.clone()
|
||||
} else {
|
||||
constructors::take_value(project, trace, self.direction, facet, &map)?
|
||||
}
|
||||
}
|
||||
SemanticDecision::TakeSource => {
|
||||
let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
|
||||
scalar::source_value(&fact, self.direction.source_layer(), &map)?
|
||||
let value = scalar::source_value(&fact, self.direction.source_layer(), &map)?;
|
||||
let mut supplied = original.clone();
|
||||
*supplied
|
||||
.pointer_mut(&roots[0])
|
||||
.ok_or("Source property is unavailable.")? = value;
|
||||
supplied
|
||||
}
|
||||
};
|
||||
if message_kind::handles(facet) && *decision == SemanticDecision::Candidate {
|
||||
message_candidate = Some((path.clone(), value.clone()));
|
||||
for path in roots {
|
||||
let value = supplied
|
||||
.pointer(&path)
|
||||
.cloned()
|
||||
.ok_or("Candidate property is no longer available.")?;
|
||||
if scalar::grouped(facet) && *decision == SemanticDecision::Candidate {
|
||||
group_candidates.push((path.clone(), value.clone()));
|
||||
}
|
||||
*final_value.pointer_mut(&path).ok_or("Final target property is unavailable; resolve the overlapping constructor decision.")? = value;
|
||||
}
|
||||
*final_value
|
||||
.pointer_mut(&path)
|
||||
.ok_or("Final target property is unavailable.")? = value;
|
||||
}
|
||||
if !form_context
|
||||
&& let Some((path, candidate)) = message_candidate
|
||||
&& let Ok(current) = message_kind::source_value(project, target)
|
||||
&& crate::forms::same_variant(&candidate, ¤t)
|
||||
&& candidate != current
|
||||
&& final_value.pointer(&path) != Some(&candidate)
|
||||
{
|
||||
return Err("The message-kind candidate and Timer Expression decision propose different timer values. Keep the kind or make their final values agree; both drafts are retained.".into());
|
||||
if !form_context {
|
||||
for (path, authored) in group_candidates {
|
||||
if !original
|
||||
.pointer(&path)
|
||||
.zip(final_value.pointer(&path))
|
||||
.is_some_and(|(original, final_value)| {
|
||||
constructors::authored_agrees(original, &authored, final_value)
|
||||
})
|
||||
{
|
||||
return Err("The constructor candidate and descendant decision propose different final values. Keep the kind or make their final values agree; both drafts are retained.".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(final_value)
|
||||
}
|
||||
@@ -513,8 +528,8 @@ impl State {
|
||||
if let Some(draft) = &mut self.draft {
|
||||
draft.set_context(context.clone());
|
||||
}
|
||||
if let Some(draft) = &mut self.message_draft {
|
||||
draft.set_context(context);
|
||||
for draft in self.group_drafts.values_mut() {
|
||||
draft.set_context(context.clone());
|
||||
}
|
||||
let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else {
|
||||
self.error = Some(
|
||||
|
||||
Reference in New Issue
Block a user