Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
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.
This commit is contained in:
@@ -104,6 +104,17 @@ pub(super) fn path(project: &Project, facet: &FacetSelector, layer: Layer) -> Op
|
||||
Family::Source | Family::Target if !project.relations.contains_key(&id) => return None,
|
||||
_ => {}
|
||||
}
|
||||
if project
|
||||
.relations
|
||||
.get(&id)
|
||||
.is_some_and(|r| r.kind() == RelationKind::ContextualLink)
|
||||
{
|
||||
return match family(facet)? {
|
||||
Family::Source => Some("/body/ContextualLink/source/occurrence".into()),
|
||||
Family::Target => Some("/body/ContextualLink/target/occurrence".into()),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
scalar::binding(project, id, &slot)
|
||||
}
|
||||
pub(super) fn label(facet: &FacetSelector) -> &'static str {
|
||||
|
||||
@@ -562,9 +562,14 @@ fn absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exa
|
||||
] {
|
||||
if value["version"] == 1 {
|
||||
value.as_object_mut().unwrap().remove("attachments");
|
||||
value.as_object_mut().unwrap().remove("operation_omission");
|
||||
} else {
|
||||
for row in value["rows"].as_array_mut().unwrap() {
|
||||
row["draft"].as_object_mut().unwrap().remove("attachments");
|
||||
row["draft"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("operation_omission");
|
||||
}
|
||||
}
|
||||
let restored = State::from_recovery(&f.p, value.clone()).unwrap();
|
||||
|
||||
@@ -668,12 +668,82 @@ impl State {
|
||||
.cloned();
|
||||
let direction = self.direction;
|
||||
let base = &self.base;
|
||||
let (source_layer, target_layer) = if direction == TransitionDirection::OaToSa {
|
||||
("Operational", "System")
|
||||
} else {
|
||||
("System", "Operational")
|
||||
};
|
||||
let mut header = column![
|
||||
text(format!("Review semantics · {} included rows", self.included_addresses().len())).size(22),
|
||||
row![pick_list(available, self.add_trace.clone(), move |choice| message(Event::ChooseTrace(choice))).placeholder("Choose another trace to add or reopen").width(Fill).text_size(12).padding(7), button(text("Open row").size(12)).padding(7).style(crate::style::nav).on_press(message(Event::OpenTrace)), button(text(if direction == TransitionDirection::OaToSa { "OA → SA" } else { "SA → OA" }).size(12)).padding(7).style(crate::style::nav).on_press(message(Event::Direction(if direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa })))].spacing(7),
|
||||
row![pick_list(choices, selected, move |choice| message(Event::Focus(RowAddress::from_trace(&base.traces[&choice.id], direction)))).placeholder("Choose a trace / review history").width(Fill).text_size(12).padding(7), checkbox(self.active_address.is_some_and(|address| self.included.contains(&address))).label("Include this row").on_toggle_maybe(self.active_address.map(|address| move |included| message(Event::Include(address, included))))].spacing(10),
|
||||
text("Switching rows retains every draft. Only included rows contribute to the one combined preview.").size(10).color(crate::style::MUTED),
|
||||
text("Review counterpart changes").size(22),
|
||||
text("Compare existing linked OA and SA objects, then choose which values to copy. This is for updating counterparts after editing; creating a counterpart is a separate action.").size(12),
|
||||
row![
|
||||
text(format!("Copy selected changes: {source_layer} → {target_layer}")).size(13),
|
||||
button(text("Reverse direction").size(12)).padding(7).style(crate::style::nav).on_press(message(Event::Direction(if direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa }))),
|
||||
].spacing(10),
|
||||
].spacing(6);
|
||||
if available.len() > 1 || self.active_address.is_none() {
|
||||
header = header.push(
|
||||
row![
|
||||
pick_list(available, self.add_trace.clone(), move |choice| message(
|
||||
Event::ChooseTrace(choice)
|
||||
))
|
||||
.placeholder("Choose a linked object to review…")
|
||||
.width(Fill)
|
||||
.text_size(12)
|
||||
.padding(7),
|
||||
button(text("Review object").size(12))
|
||||
.padding(7)
|
||||
.style(crate::style::nav)
|
||||
.on_press(message(Event::OpenTrace)),
|
||||
]
|
||||
.spacing(7),
|
||||
);
|
||||
}
|
||||
let active: iced::Element<'a, Message> = if choices.len() > 1 {
|
||||
pick_list(choices, selected, move |choice| {
|
||||
message(Event::Focus(RowAddress::from_trace(
|
||||
&base.traces[&choice.id],
|
||||
direction,
|
||||
)))
|
||||
})
|
||||
.placeholder("Choose an object already in this review")
|
||||
.width(Fill)
|
||||
.text_size(12)
|
||||
.padding(7)
|
||||
.into()
|
||||
} else if let Some(address) = self.active_address {
|
||||
text(format!(
|
||||
"{} → {}",
|
||||
super::view::choice(base, address.source).label,
|
||||
super::view::choice(base, address.target).label
|
||||
))
|
||||
.size(13)
|
||||
.width(Fill)
|
||||
.into()
|
||||
} else {
|
||||
text("Select a linked object above to compare its values.")
|
||||
.size(13)
|
||||
.width(Fill)
|
||||
.into()
|
||||
};
|
||||
header = header.push(
|
||||
row![
|
||||
active,
|
||||
checkbox(
|
||||
self.active_address
|
||||
.is_some_and(|address| self.included.contains(&address))
|
||||
)
|
||||
.label("Include this object")
|
||||
.on_toggle_maybe(
|
||||
self.active_address
|
||||
.map(|address| move |included| message(Event::Include(address, included)))
|
||||
),
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
if self.included_addresses().len() > 1 {
|
||||
header = header.push(text(format!("{} objects included. Apply saves their selected decisions together; switching objects keeps your drafts.", self.included_addresses().len())).size(11).color(crate::style::MUTED));
|
||||
}
|
||||
if let Some(notice) = &self.notice {
|
||||
header = header.push(text(notice).size(11));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,20 @@ use super::*;
|
||||
mod diagnostic_tests;
|
||||
|
||||
type Slot = (Id, FacetSelector);
|
||||
fn context_leaf_path(project: &Project, slot: &Slot) -> Option<&'static str> {
|
||||
if project.relations.get(&slot.0)?.kind() != RelationKind::ContextualLink {
|
||||
return None;
|
||||
}
|
||||
match slot.1 {
|
||||
FacetSelector::Relation(RelationFacet::Source) => {
|
||||
Some("/body/ContextualLink/source/occurrence")
|
||||
}
|
||||
FacetSelector::Relation(RelationFacet::Target) => {
|
||||
Some("/body/ContextualLink/target/occurrence")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
struct Intent {
|
||||
value: Value,
|
||||
row: RowAddress,
|
||||
@@ -47,6 +61,13 @@ fn canonical(facet: &FacetSelector, layer: Layer) -> Option<FacetSelector> {
|
||||
})
|
||||
}
|
||||
fn observe(project: &Project, slot: &Slot) -> Result<Value, String> {
|
||||
if let Some(path) = context_leaf_path(project, slot) {
|
||||
let value = record_value(project, slot.0)?
|
||||
.pointer(path)
|
||||
.cloned()
|
||||
.ok_or("Qualified endpoint leaf is absent.")?;
|
||||
return Ok(serde_json::json!({"Value":value}));
|
||||
}
|
||||
if let Some((_, facet)) = artifacts::parts(&slot.1) {
|
||||
return Ok(artifacts::observation(
|
||||
project.artifact_links.get(&slot.0),
|
||||
@@ -297,10 +318,9 @@ fn write_slot(project: &mut Project, slot: &Slot, observation: &Value) -> Result
|
||||
serde_json::to_value(order).expect("Order");
|
||||
} else {
|
||||
let path = scalar::binding(project, slot.0, &slot.1)
|
||||
.or_else(|| context_leaf_path(project, slot).map(str::to_owned))
|
||||
.ok_or("A selected property was retired by another row's constructor proposal.")?;
|
||||
*value
|
||||
.pointer_mut(&path)
|
||||
.ok_or("Selected property is no longer available.")? = supplied;
|
||||
crate::forms::set_shape_value(&mut value, &path, supplied)?;
|
||||
}
|
||||
install(project, slot.0, value)
|
||||
}
|
||||
@@ -325,6 +345,22 @@ pub(super) fn request(state: &State) -> Result<SemanticTransitionRequest, String
|
||||
})
|
||||
.collect();
|
||||
let mut request = SemanticTransitionRequest::new(transition);
|
||||
let mut omissions = BTreeMap::new();
|
||||
for address in &addresses {
|
||||
if let Some(raw) = state
|
||||
.row(*address)
|
||||
.and_then(|row| row.operation_omission.as_ref())
|
||||
&& omissions
|
||||
.insert(address.source, raw.clone())
|
||||
.is_some_and(|prior| prior != *raw)
|
||||
{
|
||||
return Err("Rows reviewing the same operation disagree on the explicit omission rationale. Retain one consistent decision before Apply.".into());
|
||||
}
|
||||
}
|
||||
request.transition.operation_omissions = omissions
|
||||
.into_iter()
|
||||
.map(|(source, rationale)| OperationOmission { source, rationale })
|
||||
.collect();
|
||||
request.references = references.clone();
|
||||
request.accepted_impacts = state.accepted_impacts.clone();
|
||||
let mut slots = BTreeMap::new();
|
||||
@@ -435,6 +471,32 @@ pub(super) fn request(state: &State) -> Result<SemanticTransitionRequest, String
|
||||
observe(&proposed, &slot)?
|
||||
};
|
||||
agree(state, &mut slots, slot.clone(), observed, address, facet)?;
|
||||
if matches!(
|
||||
facet,
|
||||
FacetSelector::Relation(
|
||||
RelationFacet::ContextSource | RelationFacet::ContextTarget
|
||||
)
|
||||
) {
|
||||
let leaf = (
|
||||
address.target,
|
||||
FacetSelector::Relation(
|
||||
if matches!(facet, FacetSelector::Relation(RelationFacet::ContextSource)) {
|
||||
RelationFacet::Source
|
||||
} else {
|
||||
RelationFacet::Target
|
||||
},
|
||||
),
|
||||
);
|
||||
let observed = observe(
|
||||
if *decision == SemanticDecision::KeepTarget {
|
||||
base
|
||||
} else {
|
||||
&proposed
|
||||
},
|
||||
&leaf,
|
||||
)?;
|
||||
agree(state, &mut slots, leaf, observed, address, facet)?;
|
||||
}
|
||||
let first_equal = row.can_acknowledge_constructor(base, facet)
|
||||
&& (*decision != SemanticDecision::Candidate
|
||||
|| observe(base, &slot)? == observe(&proposed, &slot)?);
|
||||
|
||||
@@ -463,6 +463,7 @@ fn occurrence_and_event_candidate_variants_cover_closed_families_without_touchin
|
||||
lifeline: None,
|
||||
},
|
||||
OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
@@ -519,6 +520,7 @@ fn occurrence_and_event_candidate_variants_cover_closed_families_without_touchin
|
||||
body: OccurrenceBody::Action { .. },
|
||||
..
|
||||
} => occurrence(OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
@@ -725,6 +727,7 @@ fn port_group_companions_typed_owner_context_and_direction_restrictions_are_expl
|
||||
#[test]
|
||||
fn outer_and_nested_constructor_candidates_require_explicit_resolution_and_retain_both_drafts() {
|
||||
let original = occurrence(OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
@@ -1319,6 +1322,7 @@ fn closed_data_and_event_take_composition_matches_core_across_every_kind_pair_an
|
||||
fn exchange_constructor_takes_qualified_annotation_addresses_without_remapping_expression_text() {
|
||||
for reverse in [false, true] {
|
||||
let message = occurrence(OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
|
||||
@@ -137,6 +137,7 @@ fn fixture(binding: bool) -> Fixture {
|
||||
});
|
||||
locals.push(leaf);
|
||||
OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: Some(exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
|
||||
@@ -14,6 +14,7 @@ fn fixture(oa_kind: MessageKind, sa_kind: MessageKind) -> (Project, Id, Id, Id)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
@@ -358,6 +359,7 @@ fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_dir
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&call).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
|
||||
@@ -35,6 +35,8 @@ pub(super) struct Saved {
|
||||
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,
|
||||
@@ -68,6 +70,8 @@ struct RowPayload {
|
||||
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,
|
||||
@@ -120,6 +124,7 @@ impl RowPayload {
|
||||
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,
|
||||
@@ -160,6 +165,7 @@ impl RowPayload {
|
||||
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,
|
||||
@@ -234,6 +240,7 @@ impl ReviewRow {
|
||||
.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
|
||||
@@ -378,6 +385,7 @@ impl ReviewRow {
|
||||
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.
|
||||
@@ -414,7 +422,7 @@ fn batch_limits(value: &Value) -> Result<(), String> {
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// A source-5 upgrade may reconstruct the same accepted records with a new
|
||||
/// 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(
|
||||
@@ -423,13 +431,13 @@ impl State {
|
||||
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 source-5 recovery does not match the verified legacy base."
|
||||
.into(),
|
||||
"Semantic legacy recovery does not match the verified legacy base.".into(),
|
||||
);
|
||||
}
|
||||
saved.base_fingerprint = project_fingerprint(project);
|
||||
@@ -441,8 +449,7 @@ impl State {
|
||||
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 source-5 recovery does not match the verified legacy base."
|
||||
.into(),
|
||||
"Semantic legacy recovery does not match the verified legacy base.".into(),
|
||||
);
|
||||
}
|
||||
saved.base_fingerprint = project_fingerprint(project);
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct ReviewRow {
|
||||
pub selected: Option<FacetSelector>,
|
||||
pub decisions: BTreeMap<FacetSelector, SemanticDecision>,
|
||||
pub references: BTreeMap<Id, Id>,
|
||||
pub operation_omission: Option<String>,
|
||||
pub resets: BTreeSet<FacetSelector>,
|
||||
pub staged: BTreeSet<FacetSelector>,
|
||||
pub presence: BTreeMap<FacetSelector, bool>,
|
||||
@@ -73,6 +74,7 @@ impl ReviewRow {
|
||||
selected: None,
|
||||
decisions: BTreeMap::new(),
|
||||
references: BTreeMap::new(),
|
||||
operation_omission: None,
|
||||
resets: BTreeSet::new(),
|
||||
staged: BTreeSet::new(),
|
||||
presence: BTreeMap::new(),
|
||||
@@ -133,6 +135,7 @@ impl ReviewRow {
|
||||
self.selected = None;
|
||||
self.accepted_impacts.clear();
|
||||
self.references.clear();
|
||||
self.operation_omission = None;
|
||||
self.draft = self
|
||||
.target(project)
|
||||
.and_then(|id| Draft::from_project(project, id));
|
||||
@@ -163,6 +166,15 @@ impl ReviewRow {
|
||||
pub fn update(&mut self, project: &Project, event: RowEvent) {
|
||||
let mut replan = true;
|
||||
let result = match event {
|
||||
RowEvent::OperationOmission(enabled) => {
|
||||
self.operation_omission =
|
||||
enabled.then(|| self.operation_omission.clone().unwrap_or_default());
|
||||
Ok(())
|
||||
}
|
||||
RowEvent::OperationRationale(raw) => {
|
||||
self.operation_omission = Some(raw);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(test)]
|
||||
RowEvent::Trace(choice) => {
|
||||
if !project
|
||||
@@ -734,9 +746,7 @@ impl ReviewRow {
|
||||
let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
|
||||
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;
|
||||
crate::forms::set_shape_value(&mut supplied, &roots[0], value)?;
|
||||
supplied
|
||||
}
|
||||
};
|
||||
@@ -744,11 +754,15 @@ impl ReviewRow {
|
||||
let value = supplied
|
||||
.pointer(&path)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
(matches!(facet, FacetSelector::Occurrence(OccurrenceFacet::Operation)))
|
||||
.then_some(Value::Null)
|
||||
})
|
||||
.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;
|
||||
crate::forms::set_shape_value(&mut final_value, &path, value)?;
|
||||
}
|
||||
}
|
||||
if !form_context {
|
||||
|
||||
@@ -16,7 +16,7 @@ fn enum_tag(value: &Value) -> Option<&str> {
|
||||
}
|
||||
impl Own {
|
||||
fn get(project: &Project, id: Id) -> Option<Self> {
|
||||
let (whole, element) = if let Some(e) = project.elements.get(&id) {
|
||||
let (mut whole, element) = if let Some(e) = project.elements.get(&id) {
|
||||
(serde_json::to_value(e).ok()?, true)
|
||||
} else {
|
||||
(
|
||||
@@ -24,6 +24,35 @@ impl Own {
|
||||
false,
|
||||
)
|
||||
};
|
||||
if project.elements.get(&id).is_some_and(|e| {
|
||||
e.layer() == Layer::Sa
|
||||
&& matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Call | MessageKind::Asynchronous,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
)
|
||||
}) && validation::ancestor_of_kind(project, id, Kind::Scenario)
|
||||
.and_then(|id| project.elements.get(&id))
|
||||
.is_some_and(|e| {
|
||||
matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::Scenario {
|
||||
viewpoint: ScenarioViewpoint::BoundaryInterface,
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
&& let Some(fields) = whole
|
||||
.pointer_mut("/body/Sa/Shared/Occurrence/body/Exchange")
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
fields.entry("operation").or_insert(Value::Null);
|
||||
}
|
||||
let mut prefix = vec!["body".to_string()];
|
||||
let mut value = &whole["body"];
|
||||
if element {
|
||||
@@ -114,6 +143,9 @@ fn path(own: &Own, facet: &FacetSelector) -> Option<Vec<String>> {
|
||||
F::Occurrence(OccurrenceFacet::ReplyTo) => {
|
||||
("Occurrence", &["body", "Exchange", "reply_to"])
|
||||
}
|
||||
F::Occurrence(OccurrenceFacet::Operation) => {
|
||||
("Occurrence", &["body", "Exchange", "operation"])
|
||||
}
|
||||
F::Occurrence(OccurrenceFacet::Action) => ("Occurrence", &["body", "Action", "action"]),
|
||||
F::Occurrence(OccurrenceFacet::ActionLifeline) => {
|
||||
("Occurrence", &["body", "Action", "lifeline"])
|
||||
@@ -142,8 +174,12 @@ fn path(own: &Own, facet: &FacetSelector) -> Option<Vec<String>> {
|
||||
F::State(StateFacet::Exit) => ("State", &["exit"]),
|
||||
F::State(StateFacet::DoActivity) => ("State", &["do_activity"]),
|
||||
F::Constraint(ConstraintFacet::Expression) => ("Constraint", &["expression"]),
|
||||
F::Relation(RelationFacet::Source) => ("*", &["source"]),
|
||||
F::Relation(RelationFacet::Target) => ("*", &["target"]),
|
||||
F::Relation(RelationFacet::Source) if own.kind != "ContextualLink" => ("*", &["source"]),
|
||||
F::Relation(RelationFacet::Target) if own.kind != "ContextualLink" => ("*", &["target"]),
|
||||
F::Relation(RelationFacet::ContextOwner) => ("ContextualLink", &["owner"]),
|
||||
F::Relation(RelationFacet::ContextSource) => ("ContextualLink", &["source"]),
|
||||
F::Relation(RelationFacet::ContextTarget) => ("ContextualLink", &["target"]),
|
||||
F::Relation(RelationFacet::ContextPurpose) => ("ContextualLink", &["purpose"]),
|
||||
F::Relation(RelationFacet::AllocationOrder) => ("InterfaceItemAllocation", &["order"]),
|
||||
F::Relation(RelationFacet::Transmission) => ("InterfaceItemAllocation", &["transmission"]),
|
||||
F::Relation(RelationFacet::Acquisition) => ("InterfaceItemAllocation", &["acquisition"]),
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
//! Source7 desktop composition falsifiers, using the public planner as final oracle.
|
||||
use super::*;
|
||||
|
||||
fn operation_fixture() -> Project {
|
||||
persistence::decode(include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
)))
|
||||
.unwrap()
|
||||
}
|
||||
fn operation_id(p: &Project, n: u32) -> Id {
|
||||
let alias = format!("operation_probe_{n}");
|
||||
p.elements
|
||||
.values()
|
||||
.find(|e| e.alias == alias)
|
||||
.map(|e| e.id)
|
||||
.or_else(|| {
|
||||
p.relations
|
||||
.values()
|
||||
.find(|r| r.alias == alias)
|
||||
.map(|r| r.id)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn send(state: &mut State, p: &Project, event: RowEvent) {
|
||||
state.update(p, state.address_event(event));
|
||||
}
|
||||
|
||||
fn occurrence(p: &mut Project, layer: Layer, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let mut element = p
|
||||
.make_element(Kind::Occurrence, layer, name, Some(owner))
|
||||
.unwrap();
|
||||
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
body,
|
||||
};
|
||||
let id = element.id;
|
||||
*p = execute_batch(p, &[Command::CreateElement(element)]).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_incident_leaf_keep_allows_path_change_but_conflicts_with_changed_leaf_both_directions()
|
||||
{
|
||||
let mut p = Project::empty();
|
||||
let mut layer_ids = Vec::new();
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Process
|
||||
} else {
|
||||
Kind::FunctionalChain
|
||||
};
|
||||
let owner = p.add_element(kind, layer, "Caller", None).unwrap();
|
||||
let definition = p.add_element(kind, layer, "Reusable checks", None).unwrap();
|
||||
let a = occurrence(
|
||||
&mut p,
|
||||
layer,
|
||||
definition,
|
||||
"First check",
|
||||
OccurrenceBody::Control {
|
||||
kind: ControlKind::Choice,
|
||||
},
|
||||
);
|
||||
let b = occurrence(
|
||||
&mut p,
|
||||
layer,
|
||||
definition,
|
||||
"Second check",
|
||||
OccurrenceBody::Control {
|
||||
kind: ControlKind::Choice,
|
||||
},
|
||||
);
|
||||
let r1 = occurrence(
|
||||
&mut p,
|
||||
layer,
|
||||
owner,
|
||||
"First use",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(definition),
|
||||
},
|
||||
);
|
||||
let r2 = occurrence(
|
||||
&mut p,
|
||||
layer,
|
||||
owner,
|
||||
"Second use",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(definition),
|
||||
},
|
||||
);
|
||||
let id = Id::new_v4();
|
||||
let relation = Relation {
|
||||
id,
|
||||
alias: p.fresh_alias("caller_constraint", "relation", id),
|
||||
name: "Caller constraint".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::ContextualLink {
|
||||
owner,
|
||||
source: OccurrenceEndpoint {
|
||||
references: vec![r1],
|
||||
occurrence: a,
|
||||
},
|
||||
target: OccurrenceEndpoint {
|
||||
references: vec![r2],
|
||||
occurrence: b,
|
||||
},
|
||||
purpose: ContextualLinkPurpose::Precedence {
|
||||
guard: String::new(),
|
||||
},
|
||||
},
|
||||
};
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
layer_ids.push([owner, definition, a, b, r1, r2, id]);
|
||||
}
|
||||
let attached_trace = tests::add_trace(
|
||||
&mut p,
|
||||
layer_ids[0][2],
|
||||
layer_ids[1][2],
|
||||
"Incident leaf review",
|
||||
);
|
||||
let direct_trace = tests::add_trace(
|
||||
&mut p,
|
||||
layer_ids[0][6],
|
||||
layer_ids[1][6],
|
||||
"Qualified path review",
|
||||
);
|
||||
let incident = FacetSelector::Incident {
|
||||
keys: KeyPair {
|
||||
oa: Some(layer_ids[0][6]),
|
||||
sa: Some(layer_ids[1][6]),
|
||||
},
|
||||
end: IncidentEnd::Source,
|
||||
};
|
||||
let direct = FacetSelector::Relation(RelationFacet::ContextSource);
|
||||
for reverse in [false, true] {
|
||||
let target = usize::from(!reverse);
|
||||
let mut state = State::new(&p, attached_trace).unwrap();
|
||||
if reverse {
|
||||
state.update(&p, Event::Direction(TransitionDirection::SaToOa));
|
||||
}
|
||||
send(&mut state, &p, RowEvent::EditPair(incident.clone()));
|
||||
send(
|
||||
&mut state,
|
||||
&p,
|
||||
RowEvent::Attachment(attachments::Event::Stage),
|
||||
);
|
||||
send(
|
||||
&mut state,
|
||||
&p,
|
||||
RowEvent::Decide(incident.clone(), Some(SemanticDecision::KeepTarget)),
|
||||
);
|
||||
state.update(&p, Event::ChooseTrace(view::choice(&p, direct_trace)));
|
||||
state.update(&p, Event::OpenTrace);
|
||||
for (&oa, &sa) in layer_ids[0].iter().zip(&layer_ids[1]).take(7) {
|
||||
state.references.insert(oa, sa);
|
||||
}
|
||||
state.refresh(&p);
|
||||
send(
|
||||
&mut state,
|
||||
&p,
|
||||
RowEvent::Decide(direct.clone(), Some(SemanticDecision::Candidate)),
|
||||
);
|
||||
send(&mut state, &p, RowEvent::Select(direct.clone()));
|
||||
let endpoint = OccurrenceEndpoint {
|
||||
references: vec![layer_ids[target][5]],
|
||||
occurrence: layer_ids[target][2],
|
||||
};
|
||||
send(
|
||||
&mut state,
|
||||
&p,
|
||||
RowEvent::Shape(
|
||||
"/body/ContextualLink/source".into(),
|
||||
serde_json::to_value(&endpoint).unwrap(),
|
||||
),
|
||||
);
|
||||
assert!(state.can_apply(), "{:?}", state.error);
|
||||
let expected = state.proposed.as_ref().unwrap().clone();
|
||||
assert!(
|
||||
matches!(&expected.relations[&layer_ids[target][6]].body, RelationBody::ContextualLink { source, .. } if *source == endpoint)
|
||||
);
|
||||
assert_eq!(
|
||||
execute_batch(&p, &[state.command(&p).unwrap()]).unwrap(),
|
||||
expected
|
||||
);
|
||||
let saved = state.recovery_snapshot().unwrap();
|
||||
let mut restored = State::from_recovery(&p, saved.clone()).unwrap();
|
||||
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||
assert!(restored.can_apply());
|
||||
let conflicting = OccurrenceEndpoint {
|
||||
references: vec![layer_ids[target][4]],
|
||||
occurrence: layer_ids[target][3],
|
||||
};
|
||||
send(
|
||||
&mut restored,
|
||||
&p,
|
||||
RowEvent::Shape(
|
||||
"/body/ContextualLink/source".into(),
|
||||
serde_json::to_value(conflicting).unwrap(),
|
||||
),
|
||||
);
|
||||
assert!(!restored.can_apply());
|
||||
assert!(
|
||||
restored
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("Independent row proposals disagree"),
|
||||
"{:?}",
|
||||
restored.error
|
||||
);
|
||||
assert!(restored.command(&p).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_operation_virtual_none_candidate_uses_exact_allocation_and_preserves_accepted_bytes() {
|
||||
let mut p = operation_fixture();
|
||||
let call = p
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.alias == "operation_probe_43")
|
||||
.unwrap()
|
||||
.id;
|
||||
let allocation = p
|
||||
.relations
|
||||
.values()
|
||||
.find(|r| r.alias == "operation_probe_30")
|
||||
.unwrap()
|
||||
.id;
|
||||
let owner = p
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Operational conversation", None)
|
||||
.unwrap();
|
||||
let oa = occurrence(
|
||||
&mut p,
|
||||
Layer::Oa,
|
||||
owner,
|
||||
"Operational request",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Call,
|
||||
reply_to: None,
|
||||
operation: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
let trace = tests::add_trace(&mut p, oa, call, "Service rationale");
|
||||
let before = serde_json::to_value(&p).unwrap();
|
||||
let facet = FacetSelector::Occurrence(OccurrenceFacet::Operation);
|
||||
let path = scalar::binding(&p, call, &facet).unwrap();
|
||||
assert!(
|
||||
serde_json::to_value(&p.elements[&call])
|
||||
.unwrap()
|
||||
.pointer(&path)
|
||||
.is_none()
|
||||
);
|
||||
let mut state = State::new(&p, trace).unwrap();
|
||||
send(
|
||||
&mut state,
|
||||
&p,
|
||||
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
|
||||
);
|
||||
send(&mut state, &p, RowEvent::Select(facet));
|
||||
send(
|
||||
&mut state,
|
||||
&p,
|
||||
RowEvent::Shape(path, serde_json::json!({"allocation":allocation})),
|
||||
);
|
||||
assert!(state.can_apply(), "{:?}", state.error);
|
||||
let actual = state.proposed.as_ref().unwrap();
|
||||
assert!(
|
||||
matches!(oa_core::operations::resolve(actual, call).unwrap(), oa_core::operations::OperationResolution::Selected(value) if value.allocation == allocation)
|
||||
);
|
||||
assert_eq!(serde_json::to_value(&p).unwrap(), before);
|
||||
let saved = state.recovery_snapshot().unwrap();
|
||||
let restored = State::from_recovery(&p, saved.clone()).unwrap();
|
||||
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||
assert_eq!(restored.proposed, state.proposed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_operation_omission_gui_requires_raw_rationale_and_matches_one_core_batch_after_recovery()
|
||||
{
|
||||
use crate::workflow::{Event as W, Workflow};
|
||||
let p = operation_fixture();
|
||||
let call = operation_id(&p, 43);
|
||||
let mut message = p.elements[&call].clone();
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { operation, .. },
|
||||
..
|
||||
}) = message.shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*operation = Some(OperationUse {
|
||||
allocation: Some(operation_id(&p, 30)),
|
||||
});
|
||||
let p = execute_batch(&p, &[Command::EditElement(call, message)]).unwrap();
|
||||
let mut workflow = Workflow::transition(&p, operation_id(&p, 40)).unwrap();
|
||||
assert!(workflow.update(&p, W::Apply).is_err());
|
||||
workflow
|
||||
.update(&p, W::OperationOmission(call, true))
|
||||
.unwrap();
|
||||
workflow
|
||||
.update(&p, W::OperationRationale(call, " \n ".into()))
|
||||
.unwrap();
|
||||
let saved = workflow.recovery_snapshot().unwrap();
|
||||
let mut workflow = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
||||
assert_eq!(workflow.recovery_snapshot().unwrap(), saved);
|
||||
assert!(workflow.update(&p, W::Apply).is_err());
|
||||
for (n, kind) in [(10, Kind::Actor), (11, Kind::Entity)] {
|
||||
workflow
|
||||
.update(&p, W::MappingKind(operation_id(&p, n), kind))
|
||||
.unwrap();
|
||||
}
|
||||
let rationale = " The operational conversation abstracts the technical service signature. ";
|
||||
workflow
|
||||
.update(&p, W::OperationRationale(call, rationale.into()))
|
||||
.unwrap();
|
||||
let Workflow::Transition(draft) = &workflow else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(draft.request.operation_omissions[0].rationale, rationale);
|
||||
assert!(
|
||||
draft.plan.conflicts.is_empty(),
|
||||
"{:?}",
|
||||
draft.plan.conflicts
|
||||
);
|
||||
let expected = execute_batch(&p, &draft.plan.commands).unwrap();
|
||||
let target = draft
|
||||
.plan
|
||||
.changes
|
||||
.iter()
|
||||
.find(|c| c.source == call)
|
||||
.unwrap()
|
||||
.target;
|
||||
let actual = workflow.update(&p, W::Apply).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"Behavior root adds no implicit architectural placements"
|
||||
);
|
||||
assert!(matches!(
|
||||
oa_core::operations::resolve(&actual, target).unwrap(),
|
||||
oa_core::operations::OperationResolution::Ordinary
|
||||
));
|
||||
assert_eq!(actual.elements[&call], p.elements[&call]);
|
||||
}
|
||||
@@ -919,6 +919,7 @@ fn inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approva
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&message).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: Some(exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
|
||||
@@ -278,32 +278,22 @@ impl ReviewRow {
|
||||
let mut content = column![
|
||||
header,
|
||||
row![
|
||||
action("Properties", RowEvent::Tab(Tab::Facts)),
|
||||
action("Members / rows", RowEvent::Tab(Tab::Members)),
|
||||
action("Attachments", RowEvent::Tab(Tab::Attachments)),
|
||||
action("Artifact links", RowEvent::Tab(Tab::Artifacts)),
|
||||
action(
|
||||
format!(
|
||||
"History ({}) · {} resets",
|
||||
trace
|
||||
.and_then(|t| t.baseline.as_ref())
|
||||
.and_then(|b| b.semantic.as_ref())
|
||||
.map_or(0, |b| b.facets.len()),
|
||||
self.resets.len()
|
||||
),
|
||||
RowEvent::Tab(Tab::History)
|
||||
),
|
||||
action(
|
||||
format!("Reference pairs ({})", self.references.len()),
|
||||
RowEvent::Tab(Tab::References)
|
||||
),
|
||||
action(
|
||||
format!(
|
||||
"Additional changes ({})",
|
||||
self.plan.as_ref().map_or(0, |p| p.impacts.len())
|
||||
),
|
||||
RowEvent::Tab(Tab::Impacts)
|
||||
),
|
||||
text("Review area").size(12),
|
||||
pick_list(
|
||||
vec![
|
||||
Tab::Facts,
|
||||
Tab::Members,
|
||||
Tab::Attachments,
|
||||
Tab::Artifacts,
|
||||
Tab::History,
|
||||
Tab::References,
|
||||
Tab::Impacts,
|
||||
],
|
||||
Some(self.tab),
|
||||
|tab| event(RowEvent::Tab(tab)),
|
||||
)
|
||||
.text_size(12)
|
||||
.padding(7),
|
||||
Space::new().width(Fill),
|
||||
action("Refresh preview", RowEvent::Preview)
|
||||
]
|
||||
@@ -311,6 +301,42 @@ impl ReviewRow {
|
||||
.wrap(),
|
||||
]
|
||||
.spacing(8);
|
||||
let impacts = self.plan.as_ref().map_or(0, |plan| plan.impacts.len());
|
||||
if impacts > 0 && self.tab != Tab::Impacts {
|
||||
content = content.push(action(
|
||||
format!("View {impacts} additional changes"),
|
||||
RowEvent::Tab(Tab::Impacts),
|
||||
));
|
||||
}
|
||||
if self.direction == TransitionDirection::SaToOa
|
||||
&& trace.is_some_and(|trace| {
|
||||
matches!(
|
||||
project.elements.get(&trace.sa).and_then(|e| e.shared()),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
operation: Some(_),
|
||||
message_kind: MessageKind::Call | MessageKind::Asynchronous,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
{
|
||||
content = content.push(
|
||||
checkbox(self.operation_omission.is_some())
|
||||
.label("Explicitly omit the technical operation in the OA counterpart")
|
||||
.on_toggle(|enabled| event(RowEvent::OperationOmission(enabled))),
|
||||
);
|
||||
if let Some(raw) = &self.operation_omission {
|
||||
content = content.push(
|
||||
text_input("Required engineering rationale", raw)
|
||||
.on_input(|raw| event(RowEvent::OperationRationale(raw)))
|
||||
.padding(8)
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
}
|
||||
let body = match self.tab {
|
||||
Tab::Facts => self.facts_view(project),
|
||||
Tab::References => self.references_view(project),
|
||||
|
||||
Reference in New Issue
Block a user