Add exact semantic history resets and unordered member review

This commit is contained in:
2026-09-06 08:09:44 +03:00
parent 2c0f93839f
commit 843635ed26
7 changed files with 1505 additions and 29 deletions
+6 -1
View File
@@ -718,7 +718,12 @@ fn free_string(path: &str) -> bool {
// These are choices for the closed model, not a second validation boundary.
// A current reference is always retained, even while other draft fields make it
// incompatible. Applying the complete draft remains the core validator's job.
fn reference_choices(draft: &Draft, project: &Project, path: &str, current: Id) -> Vec<Choice> {
pub(crate) fn reference_choices(
draft: &Draft,
project: &Project,
path: &str,
current: Id,
) -> Vec<Choice> {
let identities = project.identities().into_iter().collect::<BTreeMap<_, _>>();
let compatible = reference_ids(draft, project, path);
let mut objects = compatible
+159 -13
View File
@@ -9,6 +9,8 @@ use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
mod constructors;
mod history;
mod keyed;
mod message_kind;
mod scalar;
mod view;
@@ -16,6 +18,8 @@ pub(crate) use view::summary;
#[cfg(test)]
mod constructor_tests;
#[cfg(test)]
mod keyed_tests;
#[cfg(test)]
mod message_kind_tests;
#[cfg(test)]
mod tests;
@@ -24,6 +28,8 @@ mod tests;
pub enum Tab {
Facts,
References,
Members,
History,
Impacts,
}
@@ -49,6 +55,16 @@ pub enum Event {
RemoveReference(Id),
AcceptImpact(usize, bool),
Preview,
Reset(FacetSelector, bool),
Presence(FacetSelector, bool),
MemberFamily(keyed::Family),
MemberOa(Option<Choice>),
MemberSa(Option<Choice>),
Literal(String),
LiteralSide(Layer, bool),
EditPair(FacetSelector),
StageMember,
DropStaged(FacetSelector),
}
pub struct State {
@@ -59,6 +75,15 @@ pub struct State {
pub selected: Option<FacetSelector>,
pub decisions: BTreeMap<FacetSelector, SemanticDecision>,
pub references: BTreeMap<Id, Id>,
pub resets: BTreeSet<FacetSelector>,
pub staged: BTreeSet<FacetSelector>,
pub presence: BTreeMap<FacetSelector, bool>,
pub member_family: keyed::Family,
pub member_oa: Option<Choice>,
pub member_sa: Option<Choice>,
pub literal: String,
pub literal_oa: bool,
pub literal_sa: bool,
pub draft: Option<Draft>,
// Each closed constructor has an independent draft, including nested kinds.
// Scalar and sibling drafts survive decision and constructor changes.
@@ -104,6 +129,15 @@ impl State {
selected: None,
decisions: BTreeMap::new(),
references: BTreeMap::new(),
resets: BTreeSet::new(),
staged: BTreeSet::new(),
presence: BTreeMap::new(),
member_family: keyed::Family::Literal,
member_oa: None,
member_sa: None,
literal: String::new(),
literal_oa: true,
literal_sa: true,
draft: None,
group_drafts: BTreeMap::new(),
plan: None,
@@ -132,6 +166,14 @@ impl State {
fn load_trace(&mut self, project: &Project) {
self.facts.clear();
self.decisions.clear();
self.resets.clear();
self.staged.clear();
self.presence.clear();
self.member_oa = None;
self.member_sa = None;
self.literal.clear();
self.literal_oa = true;
self.literal_sa = true;
self.selected = None;
self.accepted_impacts.clear();
self.references.clear();
@@ -154,6 +196,11 @@ impl State {
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
self.references.insert(trace.oa, trace.sa);
}
self.member_family = self
.member_families(project)
.first()
.copied()
.unwrap_or(keyed::Family::Literal);
self.refresh(project);
}
@@ -191,6 +238,17 @@ impl State {
{
Err("This fact needs the structured review workflow; its history remains unchanged.".into())
} else {
if keyed::supported(&facet) {
let target_presence = self
.effective_trace(project)
.and_then(|t| capture_facet(project, t.oa, t.sa, &facet).ok())
.and_then(|f| scalar::value(&f, self.direction.target_layer()))
.and_then(|v| v.as_bool())
.unwrap_or(false);
self.presence
.entry(facet.clone())
.or_insert(target_presence);
}
self.decisions.insert(facet, decision);
Ok(())
}
@@ -274,6 +332,57 @@ impl State {
return;
}
Event::Preview => Ok(()),
Event::Reset(facet, reset) => self.reset_history(project, facet, reset),
Event::Presence(facet, present) => {
if keyed::supported(&facet)
&& self.decisions.get(&facet) == Some(&SemanticDecision::Candidate)
{
self.presence.insert(facet, present);
Ok(())
} else {
Err("Choose Set final value for this member before editing presence.".into())
}
}
Event::MemberFamily(family) => {
self.member_family = family;
replan = false;
Ok(())
}
Event::MemberOa(choice) => {
self.member_oa = choice;
replan = false;
Ok(())
}
Event::MemberSa(choice) => {
self.member_sa = choice;
replan = false;
Ok(())
}
Event::Literal(value) => {
self.literal = value;
replan = false;
Ok(())
}
Event::LiteralSide(layer, present) => {
if layer == Layer::Oa {
self.literal_oa = present;
} else {
self.literal_sa = present;
}
replan = false;
Ok(())
}
Event::EditPair(facet) => {
replan = false;
self.edit_pair(project, facet)
}
Event::StageMember => self.stage_member(project),
Event::DropStaged(facet) => {
if self.staged.remove(&facet) {
self.decisions.remove(&facet);
}
Ok(())
}
};
if let Err(error) = result {
self.error = Some(error);
@@ -322,6 +431,11 @@ impl State {
}
pub fn editable(&self, project: &Project, facet: &FacetSelector) -> bool {
if keyed::supported(facet) {
return self
.effective_trace(project)
.is_some_and(|trace| capture_facet(project, trace.oa, trace.sa, facet).is_ok());
}
self.target(project)
.and_then(|id| scalar::binding(project, id, facet))
.is_some()
@@ -345,7 +459,7 @@ impl State {
if !scalar::grouped(facet) {
return false;
}
let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else {
let Some(trace) = self.effective_trace(project) else {
return false;
};
if trace
@@ -394,7 +508,7 @@ impl State {
decision: *decision,
})
.collect(),
reset: vec![],
reset: self.resets.iter().cloned().collect(),
});
request.accepted_impacts = self.accepted_impacts.clone();
if self
@@ -423,10 +537,7 @@ impl State {
/// 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> {
let trace = self
.trace
.and_then(|id| project.traces.get(&id))
.ok_or("Choose a trace.")?;
let trace = self.effective_trace(project).ok_or("Choose a trace.")?;
let (source, target) = if self.direction == TransitionDirection::OaToSa {
(trace.oa, trace.sa)
} else {
@@ -462,6 +573,10 @@ impl State {
if *decision == SemanticDecision::KeepTarget {
continue;
}
if keyed::supported(facet) {
self.keyed_value(project, &trace, &mut final_value, facet, *decision)?;
continue;
}
let roots = constructors::roots(project, target, facet)
.ok_or("Selected property is not applicable to the target.")?;
let supplied = match decision {
@@ -480,7 +595,7 @@ impl State {
if self.can_acknowledge_constructor(project, facet) {
original.clone()
} else {
constructors::take_value(project, trace, self.direction, facet, &map)?
constructors::take_value(project, &trace, self.direction, facet, &map)?
}
}
SemanticDecision::TakeSource => {
@@ -531,15 +646,43 @@ impl State {
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 {
let Some(trace) = self.effective_trace(project) else {
self.error = Some(
"Several traces are available. Choose the correspondence and review history above."
.into(),
);
return;
};
match semantic_facets(project, trace, &self.references) {
Ok(facts) => self.facts = facts,
match semantic_facets(project, &trace, &self.references) {
Ok(mut facts) => {
// Explicit addresses survive catalog normalization, pairing changes and resets.
for selector in self.staged.iter().chain(self.decisions.keys()) {
if !facts.iter().any(|f| &f.selector == selector) {
match capture_facet(project, trace.oa, trace.sa, selector) {
Ok(current) => {
let previous = trace
.baseline
.as_ref()
.and_then(|b| b.semantic.as_ref())
.and_then(|b| {
b.facets.iter().find(|f| f.selector() == *selector)
});
facts.push(FacetDrift {
selector: selector.clone(),
drift: facet_drift(&current, previous),
current,
});
}
Err(error) => {
self.error = Some(error);
return;
}
}
}
}
facts.sort_by(|a, b| a.selector.cmp(&b.selector));
self.facts = facts;
}
Err(error) => {
self.error = Some(error);
return;
@@ -579,8 +722,11 @@ impl State {
}
pub fn command(&mut self, project: &Project) -> Result<Command, String> {
if self.decisions.is_empty() {
return Err("Choose at least one property to review.".into());
if self.decisions.is_empty() && self.resets.is_empty() {
return Err(
"Choose at least one property to review or an exact history observation to reset."
.into(),
);
}
if let Some(error) = &self.error {
return Err(error.clone());
@@ -594,7 +740,7 @@ impl State {
}
pub fn can_apply(&self) -> bool {
!self.decisions.is_empty()
(!self.decisions.is_empty() || !self.resets.is_empty())
&& self.error.is_none()
&& self
.plan
@@ -0,0 +1,43 @@
use super::*;
pub(super) fn resettable(facet: &FacetSelector) -> bool {
!matches!(facet, FacetSelector::Artifact { .. })
}
impl State {
pub(super) fn effective_trace(&self, project: &Project) -> Option<Trace> {
let mut trace = project.traces.get(&self.trace?)?.clone();
if let Some(baseline) = trace.baseline.as_mut().and_then(|b| b.semantic.as_mut()) {
baseline
.facets
.retain(|f| !self.resets.contains(&f.selector()));
}
Some(trace)
}
pub(super) fn reset_history(
&mut self,
project: &Project,
facet: FacetSelector,
reset: bool,
) -> Result<(), String> {
if !reset && self.resets.remove(&facet) {
return Ok(());
}
let exists = self
.trace
.and_then(|id| project.traces.get(&id))
.and_then(|t| t.baseline.as_ref())
.and_then(|b| b.semantic.as_ref())
.is_some_and(|b| b.facets.iter().any(|f| f.selector() == facet));
if !exists || !resettable(&facet) {
return Err("Choose an exact existing non-artifact observation to reset.".into());
}
if reset {
self.resets.insert(facet);
} else {
self.resets.remove(&facet);
}
Ok(())
}
}
@@ -0,0 +1,263 @@
//! Closed presence-only adapter. The core independently validates the full final record.
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
Member(ReferenceCollection),
Literal,
}
impl std::fmt::Display for Family {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Member(ReferenceCollection::FunctionInputItems) => "Function input items",
Self::Member(ReferenceCollection::FunctionOutputItems) => "Function output items",
Self::Member(ReferenceCollection::SignalItems) => "Signal items",
Self::Member(ReferenceCollection::ExchangeItems) => "Exchange items",
Self::Member(ReferenceCollection::TransitionTriggers) => "Transition triggers",
Self::Member(ReferenceCollection::TransitionEffects) => "Transition effects",
Self::Member(ReferenceCollection::Constrained) => "Constrained identities",
Self::Literal => "Enumeration literals",
_ => "Ordered membership (read-only)",
})
}
}
pub(super) const FAMILIES: [Family; 8] = [
Family::Member(ReferenceCollection::FunctionInputItems),
Family::Member(ReferenceCollection::FunctionOutputItems),
Family::Member(ReferenceCollection::SignalItems),
Family::Member(ReferenceCollection::ExchangeItems),
Family::Member(ReferenceCollection::TransitionTriggers),
Family::Member(ReferenceCollection::TransitionEffects),
Family::Member(ReferenceCollection::Constrained),
Family::Literal,
];
pub(super) fn family(facet: &FacetSelector) -> Option<Family> {
let family = match facet {
FacetSelector::Member { collection, .. } => Family::Member(*collection),
FacetSelector::Literal(_) => Family::Literal,
_ => return None,
};
FAMILIES.contains(&family).then_some(family)
}
pub(super) fn supported(facet: &FacetSelector) -> bool {
family(facet).is_some()
}
// Fixed paths for the seven unordered collections and the exact enum literal set.
// They are selected by the typed facet; callers cannot supply arbitrary paths.
pub(super) fn path(value: &Value, family: Family) -> Option<String> {
let prefixes = ["/body/Oa/Shared", "/body/Sa/Shared"];
let tails: &[&str] = match family {
Family::Member(ReferenceCollection::FunctionInputItems) => {
&["Port/contract/FunctionInput/incoming_items"]
}
Family::Member(ReferenceCollection::FunctionOutputItems) => {
&["Port/contract/FunctionOutput/outgoing_items"]
}
Family::Member(ReferenceCollection::SignalItems) => &["Event/body/Signal/items"],
Family::Member(ReferenceCollection::Constrained) => &["Constraint/constrained"],
Family::Literal => &["DataType/definition/Enumeration/literals"],
Family::Member(ReferenceCollection::ExchangeItems) => {
return ["Interaction", "FunctionalExchange", "BoundaryExchange"]
.iter()
.map(|kind| format!("/body/{kind}/items"))
.find(|p| value.pointer(p).is_some_and(Value::is_array));
}
Family::Member(ReferenceCollection::TransitionTriggers) => {
return value
.pointer("/body/StateTransition/triggers")
.is_some_and(Value::is_array)
.then(|| "/body/StateTransition/triggers".into());
}
Family::Member(ReferenceCollection::TransitionEffects) => {
return value
.pointer("/body/StateTransition/effects")
.is_some_and(Value::is_array)
.then(|| "/body/StateTransition/effects".into());
}
_ => return None,
};
prefixes
.iter()
.flat_map(|prefix| tails.iter().map(move |tail| format!("{prefix}/{tail}")))
.find(|p| value.pointer(p).is_some_and(Value::is_array))
}
fn key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
match facet {
FacetSelector::Member { keys, .. } => if layer == Layer::Oa { keys.oa } else { keys.sa }
.map(|id| serde_json::to_value(id).expect("Id")),
FacetSelector::Literal(keys) => if layer == Layer::Oa {
keys.oa.as_ref()
} else {
keys.sa.as_ref()
}
.map(|v| Value::String(v.clone())),
_ => None,
}
}
pub(super) fn overlap(a: &FacetSelector, b: &FacetSelector) -> bool {
if family(a).is_none() || family(a) != family(b) {
return false;
}
[Layer::Oa, Layer::Sa]
.into_iter()
.any(|layer| key(a, layer).is_some() && key(a, layer) == key(b, layer))
}
pub(super) fn project_presence(
value: &mut Value,
facet: &FacetSelector,
layer: Layer,
present: bool,
) -> Result<(), String> {
let family = family(facet).ok_or("This keyed family remains read-only.")?;
let key = key(facet, layer)
.ok_or("Choose the destination identity in Members before setting or taking presence.")?;
let path =
path(value, family).ok_or("This collection is not applicable to the destination kind.")?;
let list = value
.pointer_mut(&path)
.and_then(Value::as_array_mut)
.ok_or("Destination collection is unavailable.")?;
if present {
if !list.contains(&key) {
list.push(key);
}
} else {
list.retain(|v| v != &key);
}
Ok(())
}
impl State {
pub(super) fn keyed_value(
&self,
project: &Project,
trace: &Trace,
value: &mut Value,
facet: &FacetSelector,
decision: SemanticDecision,
) -> Result<(), String> {
let present = match decision {
SemanticDecision::KeepTarget => return Ok(()),
SemanticDecision::TakeSource => {
// A missing source identity is a target-local row, not a removal.
if key(facet, self.direction.source_layer()).is_none() {
return Ok(());
}
let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
scalar::value(&fact, self.direction.source_layer()).and_then(|v| v.as_bool()).ok_or("Source collection is not applicable; choose an explicit final presence or Keep.")?
}
SemanticDecision::Candidate => *self
.presence
.get(facet)
.ok_or("Choose final membership presence.")?,
};
project_presence(value, facet, self.direction.target_layer(), present)
}
pub(super) fn member_families(&self, project: &Project) -> Vec<Family> {
let Some(trace) = self.effective_trace(project) else {
return vec![];
};
let values: Vec<_> = [trace.oa, trace.sa]
.into_iter()
.filter_map(|id| Draft::from_project(project, id).map(|d| d.value))
.collect();
FAMILIES
.into_iter()
.filter(|family| values.iter().any(|value| path(value, *family).is_some()))
.collect()
}
pub(super) fn member_choices(&self, project: &Project, layer: Layer) -> Vec<Choice> {
let Some(trace) = self.effective_trace(project) else {
return vec![];
};
let id = if layer == Layer::Oa {
trace.oa
} else {
trace.sa
};
let Some(mut draft) = Draft::from_project(project, id) else {
return vec![];
};
if layer == self.direction.target_layer() {
draft.set_context(self.combined_value(project, true).ok());
}
let selected = if layer == Layer::Oa {
&self.member_oa
} else {
&self.member_sa
};
let Some(path) = path(&draft.context_value(), self.member_family) else {
return selected.iter().cloned().collect();
};
crate::forms::reference_choices(
&draft,
project,
&format!("{path}/0"),
selected.as_ref().map_or(Id::nil(), |c| c.id),
)
.into_iter()
.filter(|c| !c.id.is_nil())
.collect()
}
pub(super) fn edit_pair(
&mut self,
project: &Project,
facet: FacetSelector,
) -> Result<(), String> {
self.member_family = family(&facet).ok_or("This keyed family remains read-only.")?;
match facet {
FacetSelector::Member { keys, .. } => {
self.member_oa = keys.oa.map(|id| view::choice(project, id));
self.member_sa = keys.sa.map(|id| view::choice(project, id));
}
FacetSelector::Literal(keys) => {
self.literal_oa = keys.oa.is_some();
self.literal_sa = keys.sa.is_some();
self.literal = keys.oa.or(keys.sa).unwrap_or_default();
}
_ => unreachable!(),
}
self.tab = Tab::Members;
Ok(())
}
pub(super) fn stage_member(&mut self, project: &Project) -> Result<(), String> {
if !FAMILIES.contains(&self.member_family) {
return Err("This keyed family remains read-only.".into());
}
let facet = match self.member_family {
Family::Member(collection) => FacetSelector::Member {
collection,
keys: KeyPair {
oa: self.member_oa.as_ref().map(|c| c.id),
sa: self.member_sa.as_ref().map(|c| c.id),
},
},
Family::Literal => FacetSelector::Literal(KeyPair {
oa: self.literal_oa.then(|| self.literal.clone()),
sa: self.literal_sa.then(|| self.literal.clone()),
}),
};
let trace = self.effective_trace(project).ok_or("Choose a trace.")?;
capture_facet(project, trace.oa, trace.sa, &facet)?;
if let FacetSelector::Member {
keys:
KeyPair {
oa: Some(oa),
sa: Some(sa),
},
..
} = &facet
{
self.references.insert(*oa, *sa);
}
self.staged.insert(facet.clone());
self.selected = Some(facet);
self.tab = Tab::Facts;
Ok(())
}
}
@@ -0,0 +1,782 @@
use super::{tests::*, *};
use crate::{app::App, document::Document};
fn errors(s: &State) -> String {
s.error
.iter()
.chain(s.plan.iter().flat_map(|p| &p.conflicts))
.cloned()
.collect::<Vec<_>>()
.join("; ")
}
fn put(p: &mut Project, owner: Id, facet: &FacetSelector, present: bool) {
let layer = p.layer_of(owner).unwrap();
let mut value = Draft::from_project(p, owner).unwrap().value;
keyed::project_presence(&mut value, facet, layer, present).unwrap();
if let Some(element) = p.elements.get_mut(&owner) {
*element = serde_json::from_value(value).unwrap();
} else {
p.relations
.insert(owner, serde_json::from_value(value).unwrap());
}
}
fn contains(p: &Project, owner: Id, facet: &FacetSelector, oa: Id, sa: Id) -> bool {
scalar::value(
&capture_facet(p, oa, sa, facet).unwrap(),
p.layer_of(owner).unwrap(),
)
.unwrap()
.as_bool()
.unwrap()
}
fn history(p: &mut Project, trace: Id, selectors: &[FacetSelector]) {
let t = &p.traces[&trace];
let baseline = TraceBaseline {
oa: p.properties(t.oa).unwrap(),
sa: p.properties(t.sa).unwrap(),
semantic: Some(SemanticBaseline {
facets: selectors
.iter()
.map(|s| capture_facet(p, t.oa, t.sa, s).unwrap())
.collect(),
}),
};
p.traces.get_mut(&trace).unwrap().baseline = Some(baseline);
}
fn literal(name: &str) -> FacetSelector {
FacetSelector::Literal(KeyPair {
oa: Some(name.into()),
sa: Some(name.into()),
})
}
fn enum_pair() -> (Project, Id, Id, Id) {
let mut p = Project::empty();
let mut ids = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let id = p.add_element(Kind::DataType, layer, "Enum", None).unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Enumeration {
literals: vec!["retained".into()],
},
default: Some(DataValue::Enumeration("retained".into())),
unit: String::new(),
};
ids.push(id);
}
let trace = add_trace(&mut p, ids[0], ids[1], "Literal review");
(p, ids[0], ids[1], trace)
}
fn member_pair(collection: ReferenceCollection) -> (Project, Id, Id, Id, FacetSelector, [Id; 2]) {
let mut p = Project::empty();
let mut owners = vec![];
let mut keys = vec![];
let mut locals = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let work = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
let kind = match collection {
ReferenceCollection::TransitionTriggers => Kind::Event,
ReferenceCollection::TransitionEffects | ReferenceCollection::Constrained => work,
_ => Kind::ExchangeItem,
};
let key = p.add_element(kind, layer, "Mapped member", None).unwrap();
let local = p.add_element(kind, layer, "Local member", None).unwrap();
let owner = match collection {
ReferenceCollection::SignalItems => {
let id = p.add_element(Kind::Event, layer, "Signal", None).unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Event {
body: EventBody::Signal { items: vec![] },
};
id
}
ReferenceCollection::Constrained => p
.add_element(Kind::Constraint, layer, "Constraint", None)
.unwrap(),
ReferenceCollection::ExchangeItems => {
let a = p.add_element(work, layer, "From", None).unwrap();
let b = p.add_element(work, layer, "To", None).unwrap();
let id = p.relate(
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
"Exchange",
a,
b,
);
if let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = p.relations[&id].body
{
for port in [source_port, target_port] {
if let Some(SharedBody::Port { contract, .. }) =
p.elements.get_mut(&port).unwrap().shared_mut()
{
match contract {
PortContract::FunctionInput { incoming_items } => {
*incoming_items = vec![key, local]
}
PortContract::FunctionOutput { outgoing_items } => {
*outgoing_items = vec![key, local]
}
_ => unreachable!(),
}
}
}
}
id
}
ReferenceCollection::TransitionTriggers | ReferenceCollection::TransitionEffects => {
let machine = p
.add_element(Kind::StateMachine, layer, "Machine", None)
.unwrap();
let region = p
.add_element(Kind::Region, layer, "Region", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, layer, "State", Some(region))
.unwrap();
p.relate(RelationKind::StateTransition, "Transition", state, state)
}
ReferenceCollection::FunctionInputItems | ReferenceCollection::FunctionOutputItems => {
let parent = p.add_element(work, layer, "Owner", None).unwrap();
let id = Id::new_v4();
let contract = if layer == Layer::Oa {
PortContract::Legacy
} else if collection == ReferenceCollection::FunctionInputItems {
PortContract::FunctionInput {
incoming_items: vec![],
}
} else {
PortContract::FunctionOutput {
outgoing_items: vec![],
}
};
let mut port = ports::make_port(&p, id, parent, "Port", contract).unwrap();
if layer == Layer::Oa
&& let Some(SharedBody::Port { legacy, .. }) = port.shared_mut()
{
*legacy = Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
});
}
p.elements.insert(id, port);
id
}
_ => unreachable!(),
};
owners.push(owner);
keys.push(key);
locals.push(local);
}
let trace = add_trace(&mut p, owners[0], owners[1], "Membership review");
let facet = FacetSelector::Member {
collection,
keys: KeyPair {
oa: Some(keys[0]),
sa: Some(keys[1]),
},
};
(
p,
owners[0],
owners[1],
trace,
facet,
[locals[0], locals[1]],
)
}
fn stage(app: &mut App, facet: &FacetSelector) {
send(app, Event::EditPair(facet.clone()));
send(app, Event::StageMember);
assert_eq!(
state(app).selected.as_ref(),
Some(facet),
"{}",
errors(state(app))
);
}
#[test]
fn unordered_members_keep_take_candidate_preserve_other_rows_both_directions() {
for collection in [
ReferenceCollection::SignalItems,
ReferenceCollection::ExchangeItems,
ReferenceCollection::TransitionTriggers,
ReferenceCollection::TransitionEffects,
ReferenceCollection::Constrained,
] {
for reverse in [false, true] {
for decision in [
SemanticDecision::KeepTarget,
SemanticDecision::TakeSource,
SemanticDecision::Candidate,
] {
let (mut p, oa, sa, trace, facet, locals) = member_pair(collection);
let (source, target, local) = if reverse {
(sa, oa, locals[0])
} else {
(oa, sa, locals[1])
};
put(&mut p, source, &facet, true);
let local_facet = FacetSelector::Member {
collection,
keys: KeyPair {
oa: reverse.then_some(local),
sa: (!reverse).then_some(local),
},
};
put(&mut p, target, &local_facet, true);
let mut app = App::with_project(p);
let before = app.document.snapshot();
start(&mut app, source);
stage(&mut app, &facet);
decide(&mut app, facet.clone(), decision);
if decision == SemanticDecision::Candidate {
send(&mut app, Event::Presence(facet.clone(), true));
}
// A target-only Take is an explicit acknowledgment, never implicit removal.
decide(&mut app, local_facet.clone(), SemanticDecision::TakeSource);
assert!(
state(&app).can_apply(),
"{collection:?} reverse={reverse} {decision:?}: {}",
errors(state(&app))
);
let proposed = state(&app).proposed.clone().unwrap();
assert_eq!(app.document.bundle(), &before.bundle);
apply(&mut app);
assert!(app.workflow.is_none(), "{}", app.status);
assert_eq!(app.project(), &proposed);
assert_eq!(
contains(app.project(), target, &facet, oa, sa),
decision != SemanticDecision::KeepTarget
);
assert!(contains(app.project(), target, &local_facet, oa, sa));
assert!(app.project().elements.contains_key(&local));
assert_eq!(
app.project().traces[&trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets
.len(),
2
);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
}
}
}
}
#[test]
fn function_port_members_use_existing_typed_choices_and_preserve_oa_legacy() {
for collection in [
ReferenceCollection::FunctionInputItems,
ReferenceCollection::FunctionOutputItems,
] {
let (p, oa, sa, _, facet, _) = member_pair(collection);
let FacetSelector::Member { keys, .. } = &facet else {
unreachable!()
};
let mut app = App::with_project(p);
start(&mut app, oa);
assert_eq!(
state(&app).member_families(app.project()),
vec![keyed::Family::Member(collection)]
);
send(&mut app, Event::EditPair(facet.clone()));
assert!(
state(&app)
.member_choices(app.project(), Layer::Sa)
.iter()
.any(|c| Some(c.id) == keys.sa)
);
send(&mut app, Event::StageMember);
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(facet.clone(), true));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
apply(&mut app);
assert!(contains(app.project(), sa, &facet, oa, sa));
assert!(matches!(
app.project().elements[&oa].shared(),
Some(SharedBody::Port {
contract: PortContract::Legacy,
..
})
));
start(&mut app, sa);
stage(&mut app, &facet);
decide(&mut app, facet.clone(), SemanticDecision::KeepTarget);
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(facet, true));
assert!(
!state(&app).can_apply(),
"OA typed membership must remain inapplicable"
);
}
}
#[test]
fn exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss() {
for reverse in [false, true] {
let (mut p, oa, sa, _) = enum_pair();
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
let incoming = literal(" New value ");
put(&mut p, source, &incoming, true);
let removed = literal("old");
put(&mut p, target, &removed, true);
let created = literal("new value");
let mut app = App::with_project(p);
start(&mut app, source);
for (facet, decision) in [
(&incoming, SemanticDecision::TakeSource),
(&removed, SemanticDecision::Candidate),
(&created, SemanticDecision::Candidate),
] {
stage(&mut app, facet);
decide(&mut app, facet.clone(), decision);
if decision == SemanticDecision::Candidate {
send(&mut app, Event::Presence(facet.clone(), facet == &created));
}
}
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
apply(&mut app);
assert!(contains(app.project(), target, &incoming, oa, sa));
assert!(contains(app.project(), target, &created, oa, sa));
assert!(!contains(app.project(), target, &removed, oa, sa));
assert!(
matches!(app.project().elements[&target].shared(),Some(SharedBody::DataType { default: Some(DataValue::Enumeration(value)), .. }) if value=="retained")
);
let reopened = Document::from_bundle(
app.document.bundle().clone(),
source_project::ProjectLimits::default(),
)
.unwrap();
assert_eq!(reopened.project(), app.project());
}
}
#[test]
fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions() {
for reverse in [false, true] {
let (mut p, oa, sa, trace) = enum_pair();
let reset = literal("retained");
let kept = FacetSelector::Header(HeaderFacet::Name);
history(&mut p, trace, &[reset.clone(), kept.clone()]);
let previous = p.traces[&trace].baseline.clone().unwrap();
let mut app = App::with_project(p);
let mut bundle = app.document.bundle().clone();
bundle
.files
.get_mut(&bundle.entry)
.unwrap()
.insert_str(0, "// Exact review rationale survives reset.\n");
app.document.apply_bundle(bundle).unwrap();
let before = app.document.snapshot();
let revision = app.document.revision();
start(&mut app, if reverse { sa } else { oa });
send(&mut app, Event::Reset(reset.clone(), true));
assert!(state(&app).decisions.is_empty());
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
assert_eq!(
state(&app)
.facts
.iter()
.find(|f| f.selector == reset)
.unwrap()
.drift,
Drift::Unreviewed
);
send(&mut app, Event::Reset(reset.clone(), false));
assert!(!state(&app).can_apply());
assert_eq!(
state(&app)
.facts
.iter()
.find(|f| f.selector == reset)
.unwrap()
.drift,
Drift::Unchanged
);
send(&mut app, Event::Reset(reset, true));
apply(&mut app);
assert_eq!(app.document.revision(), revision + 1);
assert_eq!(app.project().elements, before.decoded.project.elements);
let after = &app.project().traces[&trace].baseline.as_ref().unwrap();
assert_eq!(after.oa, previous.oa);
assert_eq!(after.sa, previous.sa);
assert_eq!(
after
.semantic
.as_ref()
.unwrap()
.facets
.iter()
.map(FacetReview::selector)
.collect::<Vec<_>>(),
vec![kept]
);
assert!(
app.document.bundle().files[&app.document.bundle().entry]
.contains("// Exact review rationale survives reset.")
);
let accepted = app.document.snapshot();
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &accepted.bundle);
}
}
#[test]
fn historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution() {
for reverse in [false, true] {
let (mut p, oa, sa, trace, paired, _) = member_pair(ReferenceCollection::SignalItems);
let FacetSelector::Member { collection, keys } = &paired else {
unreachable!()
};
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
let previous = FacetSelector::Member {
collection: *collection,
keys: KeyPair {
oa: if reverse { None } else { keys.oa },
sa: if reverse { keys.sa } else { None },
},
};
put(&mut p, source, &paired, true);
history(&mut p, trace, std::slice::from_ref(&previous));
let mut app = App::with_project(p);
start(&mut app, source);
decide(&mut app, previous.clone(), SemanticDecision::KeepTarget);
stage(&mut app, &paired);
decide(&mut app, paired.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(paired.clone(), true));
assert!(!state(&app).can_apply());
assert_eq!(state(&app).decisions.len(), 2);
send(&mut app, Event::Decide(previous.clone(), None));
assert!(
!state(&app).can_apply(),
"Historic pair cannot silently migrate"
);
send(&mut app, Event::Reset(previous.clone(), true));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
send(&mut app, Event::Reset(previous, false));
assert!(!state(&app).can_apply());
assert!(state(&app).presence[&paired]);
let old = state(&app).resets.clone();
assert!(old.is_empty());
let historic = app.project().traces[&trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets[0]
.selector();
send(&mut app, Event::Reset(historic, true));
apply(&mut app);
assert!(contains(app.project(), target, &paired, oa, sa));
assert_eq!(
app.project().traces[&trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets[0]
.selector(),
paired
);
}
}
#[test]
fn reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft() {
for reverse in [false, true] {
let (mut p, oa, sa, trace) = enum_pair();
let ctor = FacetSelector::Data(DataFacet::Constructor);
history(&mut p, trace, std::slice::from_ref(&ctor));
let mut app = App::with_project(p);
start(&mut app, if reverse { sa } else { oa });
let created = literal("separate");
stage(&mut app, &created);
decide(&mut app, created.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(created.clone(), true));
send(&mut app, Event::Reset(ctor.clone(), true));
decide(&mut app, ctor.clone(), SemanticDecision::Candidate);
assert!(state(&app).can_acknowledge_constructor(app.project(), &ctor));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
let reviewed = &state(&app).plan.as_ref().unwrap().reviews[0];
assert_eq!(
reviewed
.acknowledged
.iter()
.cloned()
.collect::<BTreeSet<_>>(),
BTreeSet::from([ctor.clone(), created.clone()])
);
assert!(reviewed.retired.is_empty());
send(&mut app, Event::Reset(ctor.clone(), false));
assert!(!state(&app).can_apply());
assert!(state(&app).presence[&created]);
send(&mut app, Event::Reset(ctor, true));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
}
}
#[test]
fn invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable() {
let (p, oa, _, _) = enum_pair();
let mut app = App::with_project(p);
start(&mut app, oa);
let unit = FacetSelector::Data(DataFacet::Unit);
decide(&mut app, unit.clone(), SemanticDecision::Candidate);
send(
&mut app,
Event::Input(
"/body/Sa/Shared/DataType/unit".into(),
" untouched unit ".into(),
),
);
decide(&mut app, unit.clone(), SemanticDecision::KeepTarget);
let retained = literal("retained");
decide(&mut app, retained.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(retained.clone(), false));
assert!(
!state(&app).can_apply(),
"Existing default must prevent removal"
);
assert!(!state(&app).presence[&retained]);
send(&mut app, Event::Presence(retained.clone(), true));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
assert_eq!(
state(&app).draft.as_ref().unwrap().inputs["/body/Sa/Shared/DataType/unit"].raw,
" untouched unit "
);
decide(&mut app, unit, SemanticDecision::Candidate);
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
}
#[test]
fn unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority() {
let (p, oa, _, _) = enum_pair();
let mut app = App::with_project(p);
start(&mut app, oa);
let unsupported = FacetSelector::Member {
collection: ReferenceCollection::Steps,
keys: KeyPair {
oa: Some(oa),
sa: None,
},
};
decide(&mut app, unsupported.clone(), SemanticDecision::Candidate);
assert!(!state(&app).decisions.contains_key(&unsupported));
send(&mut app, Event::MemberFamily(keyed::Family::Literal));
send(&mut app, Event::Literal("unpaired".into()));
send(&mut app, Event::LiteralSide(Layer::Sa, false));
send(&mut app, Event::StageMember);
let facet = state(&app).selected.clone().unwrap();
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
assert!(!state(&app).can_apply());
send(&mut app, Event::LiteralSide(Layer::Oa, false));
send(&mut app, Event::StageMember);
assert!(state(&app).error.is_some());
assert_eq!(
state(&app).decisions.get(&facet),
Some(&SemanticDecision::Candidate)
);
}
#[test]
fn deleted_member_source_observation_removes_only_mapped_membership_both_directions() {
for reverse in [false, true] {
let (mut p, oa, sa, trace, facet, locals) = member_pair(ReferenceCollection::SignalItems);
let FacetSelector::Member { keys, .. } = &facet else {
unreachable!()
};
let (source, target, deleted, local) = if reverse {
(sa, oa, keys.sa.unwrap(), locals[0])
} else {
(oa, sa, keys.oa.unwrap(), locals[1])
};
let target_only = FacetSelector::Member {
collection: ReferenceCollection::SignalItems,
keys: KeyPair {
oa: reverse.then_some(local),
sa: (!reverse).then_some(local),
},
};
put(&mut p, source, &facet, true);
put(&mut p, target, &facet, true);
put(&mut p, target, &target_only, true);
history(&mut p, trace, std::slice::from_ref(&facet));
p = prepare_command(&p, &Command::Delete(deleted))
.unwrap()
.project;
let mut app = App::with_project(p);
start(&mut app, source);
stage(&mut app, &facet);
decide(&mut app, facet.clone(), SemanticDecision::TakeSource);
// Force mixed final-record composition, as well as the core Take projection.
let name = FacetSelector::Header(HeaderFacet::Name);
decide(&mut app, name, SemanticDecision::Candidate);
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
apply(&mut app);
assert!(!contains(app.project(), target, &facet, oa, sa));
assert!(contains(app.project(), target, &target_only, oa, sa));
let retained = if reverse {
keys.oa.unwrap()
} else {
keys.sa.unwrap()
};
assert!(app.project().elements.contains_key(&retained));
}
}
#[test]
fn stale_or_source_failed_reset_and_membership_apply_retain_exact_choices() {
let (mut p, oa, _, trace) = enum_pair();
let reset = FacetSelector::Header(HeaderFacet::Name);
history(&mut p, trace, std::slice::from_ref(&reset));
let mut app = App::with_project(p.clone());
start(&mut app, oa);
send(&mut app, Event::Reset(reset.clone(), true));
app.document
.execute(Command::Rename(oa, "Changed after preview".into()))
.unwrap();
let before = app.document.snapshot();
apply(&mut app);
assert!(state(&app).error.as_ref().unwrap().contains("stale"));
assert!(state(&app).resets.contains(&reset));
assert_eq!(app.document.bundle(), &before.bundle);
let mut app = App::with_project(p);
let bundle = app.document.bundle().clone();
let mut limits = source_project::ProjectLimits::default();
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 10;
app.document = Document::from_bundle(bundle, limits).unwrap();
start(&mut app, oa);
send(&mut app, Event::Reset(reset.clone(), true));
let added = literal(&" exact long literal ".repeat(100));
stage(&mut app, &added);
decide(&mut app, added.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(added.clone(), true));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
let before = app.document.snapshot();
apply(&mut app);
assert!(state(&app).error.is_some());
assert!(state(&app).resets.contains(&reset));
assert_eq!(
state(&app).decisions.get(&added),
Some(&SemanticDecision::Candidate)
);
assert!(state(&app).presence[&added]);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
}
#[test]
fn member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys() {
let (mut p, oa, _, trace, facet, _) = member_pair(ReferenceCollection::SignalItems);
let FacetSelector::Member { collection, keys } = &facet else {
unreachable!()
};
p.elements.get_mut(&keys.oa.unwrap()).unwrap().name = "Absent intentionally in name".into();
let name = view::title(&p, &facet);
assert!(name.contains("Absent intentionally in name"));
let deleted1 = Id::new_v4();
let deleted2 = Id::new_v4();
let old1 = FacetSelector::Member {
collection: *collection,
keys: KeyPair {
oa: Some(deleted1),
sa: None,
},
};
let old2 = FacetSelector::Member {
collection: *collection,
keys: KeyPair {
oa: Some(deleted2),
sa: None,
},
};
history(&mut p, trace, &[old1.clone(), old2.clone()]);
assert!(view::title(&p, &old1).contains(&deleted1.to_string()));
assert_ne!(view::title(&p, &old1), view::title(&p, &old2));
assert_ne!(
view::title(&p, &literal("Absent")),
view::title(
&p,
&FacetSelector::Literal(KeyPair {
oa: None,
sa: Some("Absent".into())
})
)
);
let mut app = App::with_project(p);
start(&mut app, oa);
for tab in [Tab::History, Tab::Members, Tab::Facts] {
send(&mut app, Event::Tab(tab));
let _ = state(&app).view(app.project());
}
send(&mut app, Event::Reset(old1.clone(), true));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
assert!(!state(&app).facts.iter().any(|f| f.selector == old1));
assert!(state(&app).facts.iter().any(|f| f.selector == old2));
}
#[test]
fn staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence() {
let (mut p, oa, _, trace) = enum_pair();
let reset = FacetSelector::Header(HeaderFacet::Name);
history(&mut p, trace, std::slice::from_ref(&reset));
let mut app = App::with_project(p.clone());
start(&mut app, oa);
send(&mut app, Event::Reset(reset.clone(), true));
let added = literal("staged draft");
stage(&mut app, &added);
decide(&mut app, added.clone(), SemanticDecision::Candidate);
send(&mut app, Event::Presence(added.clone(), true));
send(&mut app, Event::DropStaged(added.clone()));
assert!(state(&app).decisions.is_empty());
assert!(!state(&app).staged.contains(&added));
assert!(state(&app).presence[&added]);
assert!(state(&app).can_apply());
stage(&mut app, &added);
decide(&mut app, added.clone(), SemanticDecision::Candidate);
assert!(state(&app).presence[&added]);
p.traces
.get_mut(&trace)
.unwrap()
.baseline
.as_mut()
.unwrap()
.semantic
.as_mut()
.unwrap()
.facets
.clear();
let crate::workflow::Workflow::Semantic(s) = app.workflow.as_mut().unwrap() else {
unreachable!()
};
s.refresh(&p);
assert!(!s.can_apply());
s.tab = Tab::History;
let _ = s.view(&p);
s.update(&p, Event::Reset(reset, false));
assert!(s.can_apply(), "{}", errors(s));
assert_eq!(s.decisions.get(&added), Some(&SemanticDecision::Candidate));
}
+188 -15
View File
@@ -71,27 +71,37 @@ pub(super) fn group(facet: &FacetSelector) -> &'static str {
}
}
pub(super) fn title(project: &Project, facet: &FacetSelector) -> String {
let keys = |keys: &KeyPair<Id>| {
let keys = |keys: &KeyPair<Id>, missing: &str| {
format!(
"{} ↔ {}",
keys.oa
.map(|id| choice(project, id).label)
.unwrap_or_else(|| "Absent".into()),
.unwrap_or_else(|| missing.into()),
keys.sa
.map(|id| choice(project, id).label)
.unwrap_or_else(|| "Absent".into())
.unwrap_or_else(|| missing.into())
)
};
match facet {
FacetSelector::Member {
collection,
keys: pair,
} => format!("{} · {}", words(&format!("{collection:?}")), keys(pair)),
} => format!(
"{} · {}",
words(&format!("{collection:?}")),
keys(pair, "No identity")
),
FacetSelector::Order(collection) => format!("{} order", words(&format!("{collection:?}"))),
FacetSelector::Literal(pair) => format!(
"Literal · {} ↔ {}",
pair.oa.as_deref().unwrap_or("Absent"),
pair.sa.as_deref().unwrap_or("Absent")
pair.oa
.as_ref()
.map(|s| format!("{s:?}"))
.unwrap_or_else(|| "No identity".into()),
pair.sa
.as_ref()
.map(|s| format!("{s:?}"))
.unwrap_or_else(|| "No identity".into())
),
FacetSelector::Annotation(pair) => format!(
"Message value · {} ↔ {}",
@@ -104,11 +114,17 @@ pub(super) fn title(project: &Project, facet: &FacetSelector) -> String {
.map(|key| annotation_title(project, key))
.unwrap_or_else(|| "Absent".into())
),
FacetSelector::Binding(pair) => format!("Lifeline binding · {}", keys(pair)),
FacetSelector::Child(pair) => format!("Owned child · {}", keys(pair)),
FacetSelector::Incident { keys: pair, end } => format!("Incident {end:?} · {}", keys(pair)),
FacetSelector::Binding(pair) => format!("Lifeline binding · {}", keys(pair, "Absent")),
FacetSelector::Child(pair) => format!("Owned child · {}", keys(pair, "Absent")),
FacetSelector::Incident { keys: pair, end } => {
format!("Incident {end:?} · {}", keys(pair, "Absent"))
}
FacetSelector::Artifact { keys: pair, facet } => {
format!("Artifact {} · {}", words(&format!("{facet:?}")), keys(pair))
format!(
"Artifact {} · {}",
words(&format!("{facet:?}")),
keys(pair, "Absent")
)
}
_ => {
let json = serde_json::to_value(facet).expect("Typed selector");
@@ -199,6 +215,12 @@ fn observed(project: &Project, fact: Option<&FacetReview>, layer: Layer) -> Stri
let value = scalar::observation(fact, layer);
if value.as_str() == Some("NotApplicable") {
"Not applicable".into()
} else if keyed::supported(&fact.selector()) {
if value["Value"].as_bool() == Some(true) {
"Present".into()
} else {
"Absent".into()
}
} else {
formatted(project, &value["Value"], &scalar::references(fact))
}
@@ -237,11 +259,13 @@ impl State {
action(direction, Event::Direction(if self.direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa })),
].spacing(8),
text("Changing trace or direction starts a new review draft. Other pending edits remain separate.").size(10).color(style::MUTED),
row![action("Properties", Event::Tab(Tab::Facts)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6),
row![action("Properties", Event::Tab(Tab::Facts)), action("Members / literals", Event::Tab(Tab::Members)), action(format!("History ({})", self.resets.len()), Event::Tab(Tab::History)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6).wrap(),
].spacing(8);
let body = match self.tab {
Tab::Facts => self.facts_view(project),
Tab::References => self.references_view(project),
Tab::Members => self.members_view(project),
Tab::History => self.history_view(project),
Tab::Impacts => self.impacts_view(project),
};
content = content.push(body);
@@ -266,7 +290,7 @@ impl State {
.iter()
.filter(|f| f.drift == Drift::Unreviewed)
.count();
content = content.push(text(format!("{} selected decisions · {unreviewed} currently unreviewed facts · Independent keyed/ordered decisions and historical reset remain pending.", self.decisions.len())).size(10).color(style::MUTED));
content = content.push(text(format!("{} decisions · {} exact resets · {unreviewed} unreviewed facts · Ordered, annotation, binding and attached-record decisions remain pending.", self.decisions.len(), self.resets.len())).size(10).color(style::MUTED));
if let Some(trace) = trace {
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED));
}
@@ -426,6 +450,38 @@ impl State {
row![side(Layer::Oa, "Operational"), side(Layer::Sa, "System")].spacing(14)
]
.spacing(10);
if previous.is_some() && history::resettable(&fact.selector) {
let facet = fact.selector.clone();
detail = detail.push(
checkbox(self.resets.contains(&facet))
.label("Reset this exact previous observation before review")
.text_size(11)
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
);
}
if keyed::supported(&fact.selector) {
detail = detail.push(text("Presence changes only this collection entry; referenced objects stay intact. Take preserves a row with no source identity. An explicit source identity that is absent can remove its mapped destination entry.").size(11))
.push(action("Choose / change exact row identities…", Event::EditPair(fact.selector.clone())));
if let Some(baseline) = trace
.and_then(|t| t.baseline.as_ref())
.and_then(|b| b.semantic.as_ref())
{
for prior in &baseline.facets {
let prior = prior.selector();
if prior != fact.selector && keyed::overlap(&prior, &fact.selector) {
detail = detail.push(
checkbox(self.resets.contains(&prior))
.label(format!(
"Reset overlapping history: {}",
title(project, &prior)
))
.text_size(11)
.on_toggle(move |v| event(Event::Reset(prior.clone(), v))),
);
}
}
}
}
if let Some(proposed) = &self.proposed
&& let Some(trace) = trace
&& let Ok(after) = capture_facet(proposed, trace.oa, trace.sa, &fact.selector)
@@ -501,6 +557,19 @@ impl State {
"Leave unselected",
Event::Decide(fact.selector.clone(), None),
));
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& keyed::supported(&fact.selector)
{
let facet = fact.selector.clone();
detail = detail.push(
checkbox(self.presence.get(&facet).copied().unwrap_or(false))
.label(
"Include in destination collection (unchecked removes only this entry)",
)
.text_size(12)
.on_toggle(move |v| event(Event::Presence(facet.clone(), v))),
);
}
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& let Some(draft) = self.candidate_draft(&fact.selector)
&& let Some(paths) = self
@@ -534,17 +603,24 @@ impl State {
} else {
detail = detail.push(text(if scalar::grouped(&fact.selector) { "This constructor is unavailable on one of the current counterparts. Review the outer constructor before its nested kind." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED));
}
if self.staged.contains(&fact.selector) {
detail = detail.push(action(
"Remove staged address and its decision",
Event::DropStaged(fact.selector.clone()),
));
}
let overlaps: Vec<_> = self
.decisions
.keys()
.filter(|other| {
**other != fact.selector
&& (constructors::contains(&fact.selector, other)
|| constructors::contains(other, &fact.selector))
|| constructors::contains(other, &fact.selector)
|| keyed::overlap(other, &fact.selector))
})
.collect();
if self.decisions.contains_key(&fact.selector) && !overlaps.is_empty() {
detail = detail.push(text("Related decisions are selected. A first equal-kind acknowledgment can coexist with descendants; a replacing group requires one combined decision. Resolve overlaps explicitly; all drafts are retained.").size(11));
detail = detail.push(text("Related decisions are selected. Paired rows cannot review the same OA or SA identity twice. Replacing groups require one combined decision; first equal-kind acknowledgments may coexist with descendants. Resolve overlaps explicitly; all drafts are retained.").size(11));
for other in overlaps {
detail = detail.push(action(
format!("Leave {} unselected", title(project, other)),
@@ -557,6 +633,7 @@ impl State {
));
}
if (is_group
|| keyed::supported(&fact.selector)
|| constructors::selectors().iter().any(|group| {
self.decisions.contains_key(group) && constructors::contains(group, &fact.selector)
}))
@@ -595,10 +672,106 @@ impl State {
detail.into()
}
fn members_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
let families = self.member_families(project);
let available = families.contains(&self.member_family);
let mut content = column![
text("Review an individual member or literal").size(16),
text("Choose an address, then review its presence in Properties. This also stages a new row in an empty collection. No model object is created.").size(12),
pick_list(families, Some(self.member_family), |v| event(Event::MemberFamily(v))).text_size(12).padding(7),
].spacing(9);
if !available {
content = content.push(text("This collection is not available on the current counterparts. Choose an available collection above; existing staged addresses remain removable below.").size(12));
} else if self.member_family == keyed::Family::Literal {
content = content.push(text("Literal identity is exact, including case and spaces. Both sides use the same string; a rename requires separate old/new rows.").size(11))
.push(text_input("Exact literal", &self.literal).on_input(|v| event(Event::Literal(v))).size(12).padding(7))
.push(row![checkbox(self.literal_oa).label("Operational identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Oa,v))), checkbox(self.literal_sa).label("System identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Sa,v)))].spacing(12));
} else {
content = content.push(text("Identities may exist outside the collection. Choosing both explicitly sets their OA → SA correspondence. No identity means an unpaired side, distinct from an absent member.").size(11))
.push(row![pick_list(self.member_choices(project, Layer::Oa), self.member_oa.clone(), |v| event(Event::MemberOa(Some(v)))).placeholder("Operational identity").text_size(12).padding(7).width(Fill), action("No OA identity", Event::MemberOa(None))].spacing(7))
.push(row![pick_list(self.member_choices(project, Layer::Sa), self.member_sa.clone(), |v| event(Event::MemberSa(Some(v)))).placeholder("System identity").text_size(12).padding(7).width(Fill), action("No SA identity", Event::MemberSa(None))].spacing(7));
}
content = content.push(button(text("Use these identities and review presence").size(12)).padding([7,9]).style(style::nav).on_press_maybe(available.then_some(event(Event::StageMember))))
.push(text("Changing a row address retains its earlier decisions and drafts. Reset conflicting historical observations and leave overlapping decisions unselected explicitly.").size(11).color(style::MUTED));
if !self.staged.is_empty() {
content = content.push(text("Staged addresses in this draft").size(13));
for facet in &self.staged {
content = content.push(
row![
text(title(project, facet)).size(11).width(Fill),
action(
"Remove address / decision",
Event::DropStaged(facet.clone())
)
]
.spacing(7),
);
}
}
scrollable(content).height(305).into()
}
fn history_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
let mut content = column![text("Exact previous observations").size(16), text("Check only the observations to forget. Resets run before this review's decisions; reset alone changes history, not model values. Uncheck to restore the pending observation. Prose history remains separate.").size(12)].spacing(9);
let baseline = self
.trace
.and_then(|id| project.traces.get(&id))
.and_then(|t| t.baseline.as_ref())
.and_then(|b| b.semantic.as_ref());
if baseline.is_none_or(|b| b.facets.is_empty()) {
content =
content.push(text("No previous semantic observations on this trace.").size(12));
}
if let Some(baseline) = baseline {
for fact in &baseline.facets {
let facet = fact.selector();
let mut item = column![
text(title(project, &facet)).size(13),
text(format!(
"Operational: {}\nSystem: {}",
observed(project, Some(fact), Layer::Oa),
observed(project, Some(fact), Layer::Sa)
))
.size(11)
]
.spacing(4);
if history::resettable(&facet) {
item = item.push(
checkbox(self.resets.contains(&facet))
.label("Reset this exact observation")
.text_size(11)
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
);
} else {
item = item.push(
text("Artifact history reset remains pending with artifact review.")
.size(11),
);
}
content = content.push(container(item).padding(7));
}
}
for pending in &self.resets {
if baseline.is_none_or(|b| !b.facets.iter().any(|f| f.selector() == *pending)) {
let facet = pending.clone();
content = content.push(
checkbox(true)
.label(format!(
"Pending reset no longer in current history: {}",
title(project, &facet)
))
.text_size(11)
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
);
}
}
scrollable(content).height(305).into()
}
fn references_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
let mut content = column![
text("Reference correspondence · always Operational → System").size(15),
text("Choose explicit counterpart identities before taking reference values. Names do not establish correspondence. Changing an already reviewed keyed pair requires reset in the structured workflow.").size(12),
text("Choose explicit counterpart identities before taking reference values. Names do not establish correspondence. Changing an already reviewed keyed pair requires an exact reset in History. Existing decisions are retained; resolve their overlapping row identities explicitly.").size(12),
text_input("Filter reference choices by name or alias", &self.reference_search).on_input(|v| event(Event::ReferenceSearch(v))).padding(7).size(12),
row![
pick_list(self.all_references(project, Layer::Oa), self.reference_oa.clone(), |c| event(Event::ReferenceOa(c))).placeholder("Operational reference").padding(7).text_size(12).width(Fill),