Prepare scoped lifecycle projection and align authoring guide
This commit is contained in:
@@ -15,6 +15,9 @@ use iced::{
|
||||
use oa_core::*;
|
||||
use std::collections::BTreeMap;
|
||||
pub(crate) mod scenario;
|
||||
// Projection acceptance precedes the coordinated scenario renderer integration.
|
||||
#[cfg(test)]
|
||||
mod scenario_lifecycle;
|
||||
pub(crate) mod state_canvas;
|
||||
pub(crate) mod state_edit;
|
||||
pub(crate) mod state_menu;
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
//! Read-only lifecycle geometry. Operand screen order is never execution order.
|
||||
use oa_core::{FragmentOperator, Id, Kind, MessageKind, OccurrenceBody, Project, SharedBody};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Maps already recorded by the sequence renderer. Rows carry their owning sequence.
|
||||
pub(crate) struct Geometry<'a> {
|
||||
pub rows: &'a BTreeMap<Id, (Id, f32)>,
|
||||
pub scopes: &'a BTreeMap<Id, (f32, f32)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Span {
|
||||
pub start: f32,
|
||||
pub end: f32,
|
||||
}
|
||||
impl Span {
|
||||
fn valid(self) -> bool {
|
||||
self.start.is_finite() && self.end.is_finite() && self.start <= self.end
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Life {
|
||||
Alive,
|
||||
Absent,
|
||||
Unresolved,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Interval {
|
||||
pub scope: Id,
|
||||
pub span: Span,
|
||||
pub life: Life,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum MarkerKind {
|
||||
Birth,
|
||||
Death,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Marker {
|
||||
pub occurrence: Id,
|
||||
pub scope: Id,
|
||||
pub y: f32,
|
||||
pub kind: MarkerKind,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum UncertaintyReason {
|
||||
Conditional,
|
||||
Repeated,
|
||||
Parallel,
|
||||
MissingGeometry,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Uncertainty {
|
||||
pub scope: Id,
|
||||
pub reason: UncertaintyReason,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LaneLifetime {
|
||||
pub lifeline: Id,
|
||||
pub intervals: Vec<Interval>,
|
||||
pub markers: Vec<Marker>,
|
||||
pub uncertainties: Vec<Uncertainty>,
|
||||
}
|
||||
|
||||
/// Stable lane-ID order; source/model/history and caller-owned geometry are untouched.
|
||||
/// An opaque ScenarioUse is a local use, not a caller birth/death contract.
|
||||
pub(crate) fn project_lifetimes(
|
||||
project: &Project,
|
||||
scenario: Id,
|
||||
geometry: &Geometry<'_>,
|
||||
) -> Vec<LaneLifetime> {
|
||||
if project
|
||||
.elements
|
||||
.get(&scenario)
|
||||
.is_none_or(|e| e.kind() != Kind::Scenario)
|
||||
{
|
||||
return vec![];
|
||||
}
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.kind() == Kind::Lifeline && e.parent == Some(scenario))
|
||||
.map(|e| {
|
||||
let mut projection = Projection {
|
||||
project,
|
||||
geometry,
|
||||
lane: e.id,
|
||||
visiting: BTreeSet::new(),
|
||||
result: LaneLifetime {
|
||||
lifeline: e.id,
|
||||
intervals: vec![],
|
||||
markers: vec![],
|
||||
uncertainties: vec![],
|
||||
},
|
||||
};
|
||||
let initial = if projection.contains(scenario, Some(MarkerKind::Birth)) {
|
||||
Life::Unresolved
|
||||
} else {
|
||||
Life::Alive
|
||||
};
|
||||
projection.sequence(scenario, initial);
|
||||
projection.result
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct Projection<'a, 'g> {
|
||||
project: &'a Project,
|
||||
geometry: &'a Geometry<'g>,
|
||||
lane: Id,
|
||||
visiting: BTreeSet<Id>,
|
||||
result: LaneLifetime,
|
||||
}
|
||||
impl Projection<'_, '_> {
|
||||
fn body(&self, id: Id) -> Option<&OccurrenceBody> {
|
||||
match self.project.elements.get(&id)?.shared()? {
|
||||
SharedBody::Occurrence { body, .. } => Some(body),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn event(&self, id: Id) -> Option<MarkerKind> {
|
||||
match self.body(id)? {
|
||||
OccurrenceBody::Exchange {
|
||||
receiver: Some(line),
|
||||
message_kind,
|
||||
..
|
||||
} if *line == self.lane => match message_kind {
|
||||
MessageKind::Create => Some(MarkerKind::Birth),
|
||||
MessageKind::Delete => Some(MarkerKind::Death),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn contains(&self, scope: Id, kind: Option<MarkerKind>) -> bool {
|
||||
// Local structural walk only: no guards, iteration simulation or reuse expansion.
|
||||
let mut pending = vec![scope];
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = pending.pop() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
if self
|
||||
.event(id)
|
||||
.is_some_and(|event| kind.is_none_or(|k| event == k))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(steps) = self.project.elements.get(&id).and_then(|e| e.steps()) {
|
||||
pending.extend(steps);
|
||||
}
|
||||
if let Some(OccurrenceBody::Fragment { branches, .. }) = self.body(id) {
|
||||
pending.extend(branches);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
fn span(&self, scope: Id) -> Option<Span> {
|
||||
let &(start, end) = self.geometry.scopes.get(&scope)?;
|
||||
let span = Span { start, end };
|
||||
span.valid().then_some(span)
|
||||
}
|
||||
fn uncertain(&mut self, scope: Id, reason: UncertaintyReason) {
|
||||
let value = Uncertainty { scope, reason };
|
||||
if !self.result.uncertainties.contains(&value) {
|
||||
self.result.uncertainties.push(value);
|
||||
}
|
||||
}
|
||||
fn interval(&mut self, scope: Id, start: f32, end: f32, life: Life) {
|
||||
if start < end && start.is_finite() && end.is_finite() {
|
||||
self.result.intervals.push(Interval {
|
||||
scope,
|
||||
span: Span { start, end },
|
||||
life,
|
||||
});
|
||||
}
|
||||
}
|
||||
fn sequence(&mut self, scope: Id, entry: Life) -> Life {
|
||||
let Some(span) = self.span(scope) else {
|
||||
self.uncertain(scope, UncertaintyReason::MissingGeometry);
|
||||
return Life::Unresolved;
|
||||
};
|
||||
if !self.visiting.insert(scope) {
|
||||
self.uncertain(scope, UncertaintyReason::MissingGeometry);
|
||||
return Life::Unresolved;
|
||||
}
|
||||
let steps = self
|
||||
.project
|
||||
.elements
|
||||
.get(&scope)
|
||||
.and_then(|e| e.steps())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
// A creation directly in this sequence fixes its local birth boundary.
|
||||
// A creation only in a nested operand cannot fix its ancestor's entry.
|
||||
let mut life = if steps
|
||||
.iter()
|
||||
.any(|&id| self.event(id) == Some(MarkerKind::Birth))
|
||||
{
|
||||
Life::Absent
|
||||
} else {
|
||||
entry
|
||||
};
|
||||
let mut cursor = span.start;
|
||||
for id in steps {
|
||||
let event = self.event(id);
|
||||
let fragment = match self.body(id) {
|
||||
Some(OccurrenceBody::Fragment { operator, branches }) => {
|
||||
Some((*operator, branches.clone()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if event.is_none() && fragment.is_none() {
|
||||
continue;
|
||||
}
|
||||
let y = self
|
||||
.geometry
|
||||
.rows
|
||||
.get(&id)
|
||||
.filter(|(owner, y)| {
|
||||
*owner == scope && y.is_finite() && *y >= cursor && *y <= span.end
|
||||
})
|
||||
.map(|(_, y)| *y);
|
||||
let Some(y) = y else {
|
||||
self.uncertain(id, UncertaintyReason::MissingGeometry);
|
||||
life = Life::Unresolved;
|
||||
continue;
|
||||
};
|
||||
self.interval(scope, cursor, y, life);
|
||||
cursor = y;
|
||||
if let Some(kind) = event {
|
||||
self.result.markers.push(Marker {
|
||||
occurrence: id,
|
||||
scope,
|
||||
y,
|
||||
kind,
|
||||
});
|
||||
life = match kind {
|
||||
MarkerKind::Birth => Life::Alive,
|
||||
MarkerKind::Death => Life::Absent,
|
||||
};
|
||||
}
|
||||
if let Some((operator, branches)) = fragment {
|
||||
let entry = life;
|
||||
let mut missing = false;
|
||||
for branch in branches {
|
||||
let bounds = self
|
||||
.span(branch)
|
||||
.filter(|s| s.start >= cursor && s.end <= span.end);
|
||||
if let Some(bounds) = bounds {
|
||||
// Every sibling inherits the same entry, never its displayed predecessor's exit.
|
||||
self.sequence(branch, entry);
|
||||
cursor = bounds.end;
|
||||
} else {
|
||||
self.uncertain(branch, UncertaintyReason::MissingGeometry);
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
if self.contains(id, None) {
|
||||
let reason = match operator {
|
||||
FragmentOperator::Alternative | FragmentOperator::Optional => {
|
||||
UncertaintyReason::Conditional
|
||||
}
|
||||
FragmentOperator::Loop => UncertaintyReason::Repeated,
|
||||
FragmentOperator::Parallel => UncertaintyReason::Parallel,
|
||||
};
|
||||
self.uncertain(id, reason);
|
||||
life = Life::Unresolved;
|
||||
} else if missing {
|
||||
life = Life::Unresolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interval(scope, cursor, span.end, life);
|
||||
self.visiting.remove(&scope);
|
||||
life
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "scenario_lifecycle/tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,470 @@
|
||||
use super::*;
|
||||
use oa_core::*;
|
||||
|
||||
struct Fixture {
|
||||
p: Project,
|
||||
owner: Id,
|
||||
a: Id,
|
||||
b: Id,
|
||||
exchange: Id,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
scopes: BTreeMap<Id, (f32, f32)>,
|
||||
}
|
||||
impl Fixture {
|
||||
fn new(layer: Layer) -> Self {
|
||||
let mut p = Project::empty();
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let a = p.add_element(kind, layer, "Request", None).unwrap();
|
||||
let b = p.add_element(kind, layer, "Provide", None).unwrap();
|
||||
let owner = p
|
||||
.add_element(Kind::Scenario, layer, "Lifecycle", None)
|
||||
.unwrap();
|
||||
let exchange = p.relate(
|
||||
if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
},
|
||||
"Service",
|
||||
a,
|
||||
b,
|
||||
);
|
||||
let line = |p: &mut Project, represented| {
|
||||
let id = p
|
||||
.add_element(Kind::Lifeline, layer, "Participant", Some(owner))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(represented),
|
||||
};
|
||||
id
|
||||
};
|
||||
let a = line(&mut p, a);
|
||||
let b = line(&mut p, b);
|
||||
Self {
|
||||
p,
|
||||
owner,
|
||||
a,
|
||||
b,
|
||||
exchange,
|
||||
rows: BTreeMap::new(),
|
||||
scopes: [(owner, (0., 600.))].into(),
|
||||
}
|
||||
}
|
||||
fn occurrence(&mut self, owner: Id, y: f32, body: OccurrenceBody) -> Id {
|
||||
let mut e = self
|
||||
.p
|
||||
.make_element(
|
||||
Kind::Occurrence,
|
||||
self.p.elements[&owner].layer(),
|
||||
"Occurrence",
|
||||
Some(owner),
|
||||
)
|
||||
.unwrap();
|
||||
let id = e.id;
|
||||
*e.shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
self.p.elements.insert(id, e);
|
||||
if let Some(steps) = self.p.elements.get_mut(&owner).unwrap().steps_mut() {
|
||||
steps.push(id);
|
||||
} else if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = self.p.elements.get_mut(&owner).unwrap().shared_mut()
|
||||
{
|
||||
branches.push(id);
|
||||
}
|
||||
self.rows.insert(id, (owner, y));
|
||||
id
|
||||
}
|
||||
fn message(&mut self, owner: Id, y: f32, kind: MessageKind) -> Id {
|
||||
self.occurrence(
|
||||
owner,
|
||||
y,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(self.exchange),
|
||||
sender: Some(self.a),
|
||||
receiver: Some(self.b),
|
||||
message_kind: kind,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn fragment(&mut self, owner: Id, y: f32, operator: FragmentOperator) -> Id {
|
||||
self.occurrence(
|
||||
owner,
|
||||
y,
|
||||
OccurrenceBody::Fragment {
|
||||
operator,
|
||||
branches: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn branch(&mut self, fragment: Id, start: f32, end: f32) -> Id {
|
||||
let id = self.occurrence(
|
||||
fragment,
|
||||
start,
|
||||
OccurrenceBody::Branch {
|
||||
guard: "condition".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
self.scopes.insert(id, (start, end));
|
||||
id
|
||||
}
|
||||
fn project(&self) -> Vec<LaneLifetime> {
|
||||
let errors: Vec<_> = validation::validate(&self.p)
|
||||
.into_iter()
|
||||
.filter(|i| i.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"fixture must be ordinarily admitted: {errors:?}"
|
||||
);
|
||||
let before = self.p.clone();
|
||||
let source = source_project::format_bundle(&self.p);
|
||||
let geometry = Geometry {
|
||||
rows: &self.rows,
|
||||
scopes: &self.scopes,
|
||||
};
|
||||
let result = project_lifetimes(&self.p, self.owner, &geometry);
|
||||
assert_eq!(result, project_lifetimes(&self.p, self.owner, &geometry));
|
||||
assert_eq!(self.p, before);
|
||||
assert_eq!(source_project::format_bundle(&self.p), source);
|
||||
assert!(
|
||||
result
|
||||
.windows(2)
|
||||
.all(|pair| pair[0].lifeline < pair[1].lifeline)
|
||||
);
|
||||
result
|
||||
}
|
||||
fn lane(&self) -> LaneLifetime {
|
||||
self.project()
|
||||
.into_iter()
|
||||
.find(|lane| lane.lifeline == self.b)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
fn intervals(lane: &LaneLifetime, scope: Id) -> Vec<(f32, f32, Life)> {
|
||||
lane.intervals
|
||||
.iter()
|
||||
.filter(|i| i.scope == scope)
|
||||
.map(|i| (i.span.start, i.span.end, i.life))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_lifetime_has_exact_birth_death_boundaries_without_source_mutation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let create = f.message(f.owner, 100., MessageKind::Create);
|
||||
f.message(f.owner, 200., MessageKind::Asynchronous);
|
||||
let delete = f.message(f.owner, 300., MessageKind::Delete);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![
|
||||
(0., 100., Life::Absent),
|
||||
(100., 300., Life::Alive),
|
||||
(300., 600., Life::Absent)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
lane.markers,
|
||||
vec![
|
||||
Marker {
|
||||
occurrence: create,
|
||||
scope: f.owner,
|
||||
y: 100.,
|
||||
kind: MarkerKind::Birth
|
||||
},
|
||||
Marker {
|
||||
occurrence: delete,
|
||||
scope: f.owner,
|
||||
y: 300.,
|
||||
kind: MarkerKind::Death
|
||||
},
|
||||
]
|
||||
);
|
||||
let sender = f
|
||||
.project()
|
||||
.into_iter()
|
||||
.find(|lane| lane.lifeline == f.a)
|
||||
.unwrap();
|
||||
assert_eq!(intervals(&sender, f.owner), vec![(0., 600., Life::Alive)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternative_death_does_not_kill_its_displayed_sibling() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let fragment = f.fragment(f.owner, 100., FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment, 120., 250.);
|
||||
let right = f.branch(fragment, 300., 450.);
|
||||
f.message(left, 200., MessageKind::Delete);
|
||||
f.message(right, 350., MessageKind::Asynchronous);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, left),
|
||||
vec![(120., 200., Life::Alive), (200., 250., Life::Absent)]
|
||||
);
|
||||
assert_eq!(intervals(&lane, right), vec![(300., 450., Life::Alive)]);
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![(0., 100., Life::Alive), (450., 600., Life::Unresolved)]
|
||||
);
|
||||
assert_eq!(
|
||||
lane.uncertainties,
|
||||
vec![Uncertainty {
|
||||
scope: fragment,
|
||||
reason: UncertaintyReason::Conditional
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_birth_does_not_infer_absence_in_siblings_or_ancestors() {
|
||||
for operator in [FragmentOperator::Alternative, FragmentOperator::Optional] {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let fragment = f.fragment(f.owner, 100., operator);
|
||||
let left = f.branch(fragment, 120., 250.);
|
||||
let right = f.branch(fragment, 300., 450.);
|
||||
f.message(left, 200., MessageKind::Create);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, left),
|
||||
vec![(120., 200., Life::Absent), (200., 250., Life::Alive)]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, right),
|
||||
vec![(300., 450., Life::Unresolved)]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![(0., 100., Life::Unresolved), (450., 600., Life::Unresolved)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_lifetime_is_local_and_warning_bearing_loops_remain_drawable() {
|
||||
for balanced in [true, false] {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let fragment = f.fragment(f.owner, 100., FragmentOperator::Loop);
|
||||
let iteration = f.branch(fragment, 120., 450.);
|
||||
let create = f.message(iteration, 160., MessageKind::Create);
|
||||
f.message(iteration, 250., MessageKind::Asynchronous);
|
||||
if balanced {
|
||||
f.message(iteration, 350., MessageKind::Delete);
|
||||
}
|
||||
let lane = f.lane();
|
||||
assert_eq!(lane.markers.len(), if balanced { 2 } else { 1 });
|
||||
assert_eq!(lane.markers[0].occurrence, create);
|
||||
assert!(lane.uncertainties.contains(&Uncertainty {
|
||||
scope: fragment,
|
||||
reason: UncertaintyReason::Repeated
|
||||
}));
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner).last().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert_eq!(
|
||||
validation::validate(&f.p)
|
||||
.iter()
|
||||
.any(|i| i.code == "lifeline_repetition"),
|
||||
!balanced
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_cleanup_inside_loop_does_not_claim_balanced_or_definite_exit() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let repeated = f.fragment(f.owner, 100., FragmentOperator::Loop);
|
||||
let iteration = f.branch(repeated, 120., 500.);
|
||||
f.message(iteration, 160., MessageKind::Create);
|
||||
let optional = f.fragment(iteration, 280., FragmentOperator::Optional);
|
||||
let cleanup = f.branch(optional, 320., 400.);
|
||||
f.message(cleanup, 350., MessageKind::Delete);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, cleanup),
|
||||
vec![(320., 350., Life::Alive), (350., 400., Life::Absent)]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, iteration),
|
||||
vec![
|
||||
(120., 160., Life::Absent),
|
||||
(160., 280., Life::Alive),
|
||||
(400., 500., Life::Unresolved)
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
validation::validate(&f.p)
|
||||
.iter()
|
||||
.any(|i| i.code == "lifeline_repetition")
|
||||
);
|
||||
assert!(lane.uncertainties.contains(&Uncertainty {
|
||||
scope: optional,
|
||||
reason: UncertaintyReason::Conditional
|
||||
}));
|
||||
assert!(lane.uncertainties.contains(&Uncertainty {
|
||||
scope: repeated,
|
||||
reason: UncertaintyReason::Repeated
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_free_fragments_do_not_poison_other_lanes() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let fragment = f.fragment(f.owner, 100., FragmentOperator::Parallel);
|
||||
let left = f.branch(fragment, 120., 250.);
|
||||
let right = f.branch(fragment, 300., 450.);
|
||||
f.message(left, 200., MessageKind::Delete);
|
||||
let result = f.project();
|
||||
let sender = result.iter().find(|lane| lane.lifeline == f.a).unwrap();
|
||||
assert!(sender.intervals.iter().all(|i| i.life == Life::Alive));
|
||||
assert!(sender.uncertainties.is_empty());
|
||||
let target = result.iter().find(|lane| lane.lifeline == f.b).unwrap();
|
||||
assert!(target.uncertainties.contains(&Uncertainty {
|
||||
scope: fragment,
|
||||
reason: UncertaintyReason::Parallel
|
||||
}));
|
||||
assert_eq!(intervals(target, right), vec![(300., 450., Life::Alive)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_receiver_is_authoritative_even_with_missing_exchange() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let create = f.message(f.owner, 100., MessageKind::Create);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = f.p.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
}
|
||||
assert_eq!(f.lane().markers[0].occurrence, create);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
receiver, exchange, ..
|
||||
},
|
||||
..
|
||||
}) = f.p.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*receiver = None;
|
||||
*exchange = Some(f.exchange);
|
||||
}
|
||||
assert!(
|
||||
f.lane().markers.is_empty(),
|
||||
"exchange endpoints never invent a receiver"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_geometry_is_reported_without_nonfinite_or_guessed_markers() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let create = f.message(f.owner, 100., MessageKind::Create);
|
||||
f.rows.insert(create, (f.owner, f32::NAN));
|
||||
let lane = f.lane();
|
||||
assert!(lane.markers.is_empty());
|
||||
assert_eq!(
|
||||
lane.uncertainties,
|
||||
vec![Uncertainty {
|
||||
scope: create,
|
||||
reason: UncertaintyReason::MissingGeometry
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![(0., 600., Life::Unresolved)]
|
||||
);
|
||||
f.scopes.remove(&f.owner);
|
||||
let lane = f.lane();
|
||||
assert!(lane.intervals.is_empty());
|
||||
assert_eq!(lane.uncertainties[0].scope, f.owner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_use_is_opaque_and_never_imports_callee_lifecycle_events() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let callee =
|
||||
f.p.add_element(Kind::Scenario, Layer::Oa, "Callee", None)
|
||||
.unwrap();
|
||||
let remote_a =
|
||||
f.p.add_element(Kind::Lifeline, Layer::Oa, "Remote A", Some(callee))
|
||||
.unwrap();
|
||||
let remote_b =
|
||||
f.p.add_element(Kind::Lifeline, Layer::Oa, "Remote B", Some(callee))
|
||||
.unwrap();
|
||||
for (remote, local) in [(remote_a, f.a), (remote_b, f.b)] {
|
||||
let body = f.p.elements[&local].shared().unwrap().clone();
|
||||
*f.p.elements.get_mut(&remote).unwrap().shared_mut().unwrap() = body;
|
||||
}
|
||||
f.occurrence(
|
||||
callee,
|
||||
200.,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: Some(remote_a),
|
||||
receiver: Some(remote_b),
|
||||
message_kind: MessageKind::Create,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
f.occurrence(
|
||||
f.owner,
|
||||
100.,
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(callee),
|
||||
bindings: vec![
|
||||
LifelineBinding {
|
||||
referenced: remote_a,
|
||||
local: f.a,
|
||||
},
|
||||
LifelineBinding {
|
||||
referenced: remote_b,
|
||||
local: f.b,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
let lane = f.lane();
|
||||
assert_eq!(intervals(&lane, f.owner), vec![(0., 600., Life::Alive)]);
|
||||
assert!(lane.markers.is_empty());
|
||||
assert!(lane.uncertainties.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_retains_exact_authored_source_and_does_not_enter_history() {
|
||||
use crate::document::Document;
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
f.message(f.owner, 100., MessageKind::Create);
|
||||
f.message(f.owner, 300., MessageKind::Delete);
|
||||
let mut source = source_project::format_bundle(&f.p);
|
||||
let text = source.files.get_mut(&source.entry).unwrap();
|
||||
*text = format!("// Retained author note — 雨\r\n{text}\n");
|
||||
let document =
|
||||
Document::from_bundle(source.clone(), source_project::ProjectLimits::default()).unwrap();
|
||||
let accepted = document.project().clone();
|
||||
let geometry = Geometry {
|
||||
rows: &f.rows,
|
||||
scopes: &f.scopes,
|
||||
};
|
||||
let result = project_lifetimes(document.project(), f.owner, &geometry);
|
||||
assert_eq!(result, f.project());
|
||||
assert_eq!(document.bundle(), &source);
|
||||
assert_eq!(document.project(), &accepted);
|
||||
assert!(!document.can_undo());
|
||||
assert!(!document.can_redo());
|
||||
}
|
||||
@@ -491,18 +491,18 @@ fn article(topic: Topic) -> Article {
|
||||
&[
|
||||
"Open Behavior & scenarios, choose Scenario under Create element and add it at a legal root/owner. Select the scenario in the behavior selector.",
|
||||
"Use + Lifeline and choose the participant or work it represents. Choices follow the scenario's viewpoint: operational activities, operational participants, system functions or system boundary participants.",
|
||||
"Choose + Message and its style. Call + correlated reply creates both occurrences in one edit. Select the exchange and compatible sending/receiving lifelines. Reply to existing call offers valid unanswered calls.",
|
||||
"Use Action, Observe state or Fragment for other behavior. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.",
|
||||
"Choose a message style and Draw message. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. The form retains the chosen position while you select the exchange and details. Escape cancels the gesture.",
|
||||
"Choose a message style in the tool selector. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. Complete the exchange and details in the form, which retains that sequence position. Escape cancels the gesture.",
|
||||
"Call + correlated reply creates both occurrences in one edit. Reply to existing call offers valid unanswered calls. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.",
|
||||
"Choose Add action, Observe state, Reuse scenario or Add fragment and click the intended sequence position. Clicking an action or observation on a lifeline also selects that lifeline. Complete any remaining choices before Create.",
|
||||
"In Select / reorder, click an occurrence and drag its ↕ handle to another position within the same sequence. The ordered list's up/down controls provide another way to reorder. Select an occurrence to inspect its particular use of the exchange, including its message values.",
|
||||
],
|
||||
),
|
||||
walkthrough(
|
||||
"Describe alternatives",
|
||||
&[
|
||||
"Choose Fragment and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
|
||||
"Use the Destination selector in subsequent message or action forms to choose the intended operand. New occurrences append to that operand's ordered behavior.",
|
||||
"Use Reuse scenario to refer to existing behavior. Bind every referenced lifeline to a compatible local lifeline explicitly. Open source follows the original scenario without copying its contents.",
|
||||
"Choose Add fragment, click its sequence position and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
|
||||
"Click the highlighted position inside an operand to place the next message, action or reference there. The form shows its destination and insertion position. Append to this sequence instead explicitly moves the pending insertion to that sequence's end.",
|
||||
"Choose Reuse scenario and click its position, then bind every referenced lifeline to a compatible local lifeline explicitly. Select the resulting use and choose Open definition. Back to caller returns to that exact use and its viewport after you apply or cancel pending edits.",
|
||||
"Inspect the scenario projection and Model checks. State exactly what each guard means, including units and assumptions where relevant.",
|
||||
],
|
||||
),
|
||||
@@ -696,6 +696,10 @@ fn article(topic: Topic) -> Article {
|
||||
"Map references by meaning",
|
||||
"Reference mapping connect exact OA and SA identities used by the proposed change. Similar names are not sufficient. Map the records that play the same engineering role, including enclosing fields in qualified message-value paths. Mapping a referenced record does not authorize creating, moving or editing that record. If it needs a counterpart, establish that correspondence first.",
|
||||
),
|
||||
explain(
|
||||
"Edit stored observations deliberately",
|
||||
"In a trace's Properties, Edit stored history (advanced)… edits the observations retained by that trace. Choose the fact and enter its Operational and System observations explicitly. Not applicable, an observed absent value, and no stored fact have different meanings. Historical references may retain the exact UUID of a deleted record. Apply changes validates the complete history; Undo restores it. These history edits leave both counterpart records unchanged. Use Review counterpart changes… for Keep/Take decisions about their current engineering content.",
|
||||
),
|
||||
walkthrough(
|
||||
"Review one message value or lifeline binding",
|
||||
&[
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Run from any directory with Python 3: semantic_schema.py [--check] [--validate].
|
||||
--validate additionally runs source-7 structural fixtures with jsonschema.
|
||||
This generates documentation only, never runtime selectors or model mutations.
|
||||
The desktop stored-history editor reads the checked-in descriptor for field
|
||||
shapes. Core types and validation remain authoritative for model mutations.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
|
||||
Reference in New Issue
Block a user