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
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Scenario lifecycle geometry proposal — 2026-09-10
|
||||
|
||||
Author: Codex-956atf /root/palette_repair. Status: source/design review only; not implemented, compiled or native-tested. Ownership is this report only. Current scenario changes belong to behavior_audit; the subsequent scenario layout work belongs to the separately coordinated Codex-9e8r6t session.
|
||||
|
||||
## Decision
|
||||
|
||||
Render Create/Delete as lifecycle events with scope-aware live segments, separate from neutral lane guides. Keep every existing lifeline's identity and editing target available. Do not shorten one full-height line using the earliest creation and latest deletion in screen coordinates: stacked ALT operands are alternatives, not successive execution.
|
||||
|
||||
The first complete candidate should support exact root lifetime boundaries, operand-local boundaries, explicit unresolved continuation at conditional merges, and a source-navigation route for lifecycle hidden inside references. This is a derived desktop projection. No Project, occurrence, source, REST, recovery or history schema changes are needed. Rendering must never create/delete lifeline elements or infer new semantic lifecycle events.
|
||||
|
||||
## Source evidence and limits
|
||||
|
||||
- `crates/oa-desktop/src/behavior.rs`, `Scene::sequence`, currently draws every lane from y=85 to the scene bottom and puts its header at y=15. `Scene::occurrences` labels Create/Delete but renders ordinary message endpoints. `rows` and `sequence_bounds` already retain semantic sequence identities and row extents. `activation_sequence` provides a useful example of independent branch traversal, but lifecycle is a different analysis.
|
||||
- `crates/oa-core/src/validation.rs`, `possibly_before`, distinguishes exclusive ALT/Optional operands, possible parallel ordering, and ordered sibling contributions. `guaranteed_before` deliberately does not promote a conditional branch event into an unconditional fact after its fragment.
|
||||
- The same file's `validate_message` rejects a use possibly before Create or after Delete, including local actions/states and ScenarioUse bindings. It emits `lifeline_repetition` warnings for unbalanced loop lifetime events; these warnings are accepted model states, not rendering failures.
|
||||
- `validate_scenario_use` validates target/viewpoint/binding identities and recursion. A binding counts as a local lifeline use. It does not define a caller-visible birth/death contract, or inline a callee lifetime into the caller. A presentation change must not invent that contract.
|
||||
- Fragment guards are strings. The renderer cannot prove guard coverage, numeric loop count, or a branch's execution merely from its displayed position. In particular, do not treat an `else` spelling as an execution proof that the core does not supply.
|
||||
|
||||
## Geometry and selection rules
|
||||
|
||||
Maintain a neutral, low-contrast dotted lane guide for navigation and insertion, distinct from the stronger active-lifetime spine. A small legend or selected-lifeline description must explain that the guide is an editing axis, not evidence of existence. Without this distinction a shortened lifetime would make future Create placement undiscoverable or make unresolved paths appear definitely alive.
|
||||
|
||||
Keep the existing top identity header as an explicitly neutral lane label. At a Create receiver row draw a compact birth capsule carrying the lifeline's short name and start its active spine there. Route the Create message into the capsule's sender-facing boundary; do not end the arrow at an unrelated center behind the label. At Delete draw an X centered on the receiver lane and stop the active spine there. An ordinary message keeps its existing arrow convention. For a self-Create/Delete use a local elbow with sufficient clearance so the event marker and arrowhead remain distinguishable.
|
||||
|
||||
Each birth/death marker carries the original lifecycle occurrence ID for selection and properties. The lane header/guide carries the lifeline ID. Hit-test markers before guides; never reuse a fake element/relation ID. `lane_at` remains based on the lane map and visible canvas bounds rather than active spine segments, so users can insert a Create before the current birth or inspect an unused lane. Core validation remains the acceptance boundary for those edits.
|
||||
|
||||
Do not let ordinary guide lines receive message arrowheads. Separate guide/lifetime geometry from `Line` message records, or introduce an explicit render role; overloading `dashed` would confuse reply dashes with lane semantics. Keep call activation geometry unchanged except for clipping/overlap QA. Lifetime is not call activation.
|
||||
|
||||
## Conservative structural projection
|
||||
|
||||
Use a pure per-lifeline traversal over the scenario/operand tree. Track a derived presentation state `Alive`, `Absent` or `Unresolved`. `Unresolved` means the stored local structure does not establish one lifetime across every represented path; it is not an invalid model state. This state exists only in the renderer.
|
||||
|
||||
Initial state: a root-owned Create establishes absence before that event. Lifecycle classification is per exact local receiver lifeline: unrelated lane events and opaque ScenarioUse records do not change it. A lane with no Create anywhere in the local tree uses the ordinary pre-existing-lifeline convention. A lane whose birth is conditional/repeated starts unresolved outside that scope. When evidence is insufficient, retain the guide and local markers instead of asserting global absence or presence. Do not turn missing/partial receiver data into a new boundary. Conversely, an explicit receiver with an unresolved exchange still has its authored lifecycle event; relation completeness is a separate validation concern.
|
||||
|
||||
| Structure | Concrete rule |
|
||||
| --- | --- |
|
||||
| Root ordered sequence | Traverse by stored steps. Create changes that local path to Alive at its row; Delete changes it to Absent. Draw only Alive intervals with the active spine; Absent intervals retain the neutral guide. Later explicit root events may establish a definite state again. A simple root Create → use → Delete therefore has exactly one live interval. |
|
||||
| ALT | Give each operand the same entry state and project each independently. A deletion in the upper operand never removes the lower operand's spine. If any operand affects lifetime, the enclosing continuation is Unresolved unless unconditional knowledge is separately established by a later root event. This conservative merge also avoids assuming guards are exhaustive. |
|
||||
| Optional | Draw its operand as one possible path and retain the bypass possibility. A lifetime change inside it cannot globally create/delete the lane after the frame. The continuation becomes Unresolved. |
|
||||
| Loop | Show one labeled iteration's local lifetime. A same-operand Create → use → Delete is a local interval, not a global one spanning every iteration. After a lifecycle-bearing loop, use Unresolved continuation because zero/exact iteration count and loop entry lifetime are not modeled. An unbalanced event stays visible and retains the existing repetition warning; do not silently repair it or suppress it. Optional cleanup inside a loop does not prove balance. |
|
||||
| Parallel | Draw each operand independently from the entry context. Screen order supplies no interleaving. Any lifecycle-affecting operand makes aggregate continuation Unresolved; do not choose last screen event as final state. No lifecycle events means ordinary continuation. Existing core validation still rejects lifecycle/use conflicts across parallel operands. |
|
||||
| Nested fragments | Apply the same rules recursively. An event clips only its current structural path; unresolved effects propagate outward. Each sibling starts from its enclosing entry context, not from its preceding displayed sibling's exit. Never connect a dead segment in one operand directly to the next operand's active segment as if it were resurrection. |
|
||||
| ScenarioUse | Keep the reference frame opaque. Show its binding usage/identity and the existing Open/Back navigation. Do not map the callee's internal row Y or births/deaths onto the caller. If indicating referenced lifecycle effects is useful, use a scope label such as “Lifecycle inside reference”; it must not claim the caller lane was created/deleted. The callee renders its own lifetimes when opened. |
|
||||
|
||||
At unresolved intervals draw only the neutral guide and a concise scope/selection explanation, such as “Lifetime depends on operand.” Do not use a continuous active spine that suggests unconditional survival. Within an operand, a first local Create establishes its birth boundary; geometry before it in that operand is absent/unresolved as appropriate, without retroactively changing sibling or ancestor geometry.
|
||||
|
||||
This proposal intentionally underclaims rather than inventing full execution analysis. A later optimization may prove additional definite joins only if it shares an authoritative semantic rule with core. It should not be a second private simulation engine in the desktop.
|
||||
|
||||
## Minimum implementation partition
|
||||
|
||||
1. Add a small pure module under `behavior/scenario/` (for example `lifecycle.rs`) returning scoped intervals and marker descriptors keyed by lifeline/occurrence/sequence IDs. Inputs: Project, visible scenario and row/scope geometry. It performs no commands, UUID generation, serialization writes or source formatting.
|
||||
2. After `Scene::occurrences` records positions, call this projection from `Scene::sequence`. If current `sequence_bounds` omit a needed fragment/operand frame bound, record that bound during the existing recursion; do not derive nesting from screen overlap.
|
||||
3. Extend Scene with explicit lane guides/lifetime markers, or a typed rendering role. Adapt message endpoint geometry only for resolved local Create receiver capsules. Paint guide → lifetime spine → messages → markers/labels, with deliberate marker hit priority.
|
||||
4. Preserve `lane_at`, slots, zoom, scroll, pending placement, and pointer-context cancellation behavior. Add selected-marker explanations through existing selection/properties affordances. Coordinate this with the separately owned scenario pane/layout change before editing shared `behavior.rs`.
|
||||
5. No core edit should be required for this conservative candidate. If definite post-reference or exhaustive-ALT effects are requested, stop treating them as geometry and propose the semantic contract explicitly first.
|
||||
|
||||
## Exact fixtures and assertions
|
||||
|
||||
Use ordinary Command/Document fixtures; assert projection leaves both Project and exact SourceBundle unchanged. For mutations followed by reprojection, one Undo/Redo must restore exact source plus prior geometry. Reuse existing core fixtures rather than weakening admission to make drawings easier.
|
||||
|
||||
1. Root: sender A, receiver B; Create B, Action on B, Delete B. Assert one active B interval bounded by the two occurrence rows, one birth capsule, one X, guides before/after, no change to A lifetime. No-Create pre-existing B + Delete ends at Delete. Root Create without Delete continues to scenario bottom.
|
||||
2. ALT: delete B in left operand, ordinary use B in right. This is valid in `conditional_lifetime_changes_conflict_with_later_or_parallel_use_not_exclusive_use`. Assert right remains independently active; no active connection between operand layouts; continuation is unresolved. Swap displayed operands and assert identical semantic per-operand intervals.
|
||||
3. Optional: delete B only inside operand. Assert bypass remains represented and post-frame lifetime is unresolved. A later B use must still be rejected by core exactly as before; projection does not legitimize it.
|
||||
4. Conditional birth: Create B in one ALT/Optional operand, no creation in another/bypass. Assert only the creating path gets a birth marker; no global header relocation or claimed birth before the whole fragment.
|
||||
5. Loop: Create B → use B → Delete B in one iteration. Assert local interval and iteration scope. Unbalanced Create and unbalanced Delete remain drawable with `lifeline_repetition`. Nest Optional Delete after loop Create; warning and unresolved continuation remain, matching `optional_cleanup_does_not_claim_a_repeated_lifetime_is_balanced`.
|
||||
6. Parallel: lifecycle in one operand and conflicting use in another remains core-invalid. For a valid non-conflicting model, geometry never takes the later screen operand as a happens-after statement. Nested ALT inside PAR and PAR inside ALT terminate deterministically.
|
||||
7. Reuse: two distinct ScenarioUse occurrences bind the same referenced lifeline to different local lifelines. Assert no caller birth/death is inferred; caller use and binding IDs remain distinct; opening either reference shows its own source projection and Back restores the exact use. A caller Delete followed by a bound use remains core-invalid.
|
||||
8. Optional/incomplete records: unresolved receiver, absent target relation, empty fragment, empty operand, lifeline without represented element. An explicit receiver keeps its Create/Delete marker even when exchange=None; receiver=None never borrows an endpoint from the exchange. Retain existing placeholders, no panic, no guessed lifecycle boundary. Test accepted warning-bearing models; do not bypass core errors in native acceptance.
|
||||
9. Interaction: birth/X clicks select lifecycle occurrence; top header/guide selects lifeline; Add Action/Message/Create still reaches slots outside the active interval and core decides validity. At 0.5×/1×/2×, hit radius stays screen-sized. Scroll/resize/tool change between press/release must cancel stale gestures.
|
||||
10. Projection repeatability: unchanged model produces identical descriptors; rename only changes captions; alternate UUID insertion/map iteration order cannot reorder semantic paths. Read-only projection does not touch source bytes/history/camera.
|
||||
|
||||
Useful existing core tests: `create_delete_and_local_timer_messages_enforce_lifetime_rules`, `conditional_lifetime_changes_conflict_with_later_or_parallel_use_not_exclusive_use`, `repeated_lifecycle_requires_per_iteration_balance_without_prohibiting_balanced_loops`, `optional_cleanup_does_not_claim_a_repeated_lifetime_is_balanced`, and scenario reuse binding/deletion tests in `crates/oa-core/tests/release_scenarios.rs`.
|
||||
|
||||
## Native questions and acceptance boundary
|
||||
|
||||
- At 1100×700 and 1440×900, can a new user distinguish an editing guide from the active spine and find the relevant operand? Does a birth capsule fit without covering the preceding slot, message label or pending insertion marker?
|
||||
- Check self-Create/Delete, long Unicode names, adjacent births on neighboring lanes, nested fragments and selected activation bars at minimum zoom. Marker/capsule collision needs actual rendering QA, not just coordinate tests.
|
||||
- Confirm neutral top lane labels are clearly identities, not birth boxes. If users still read them as existence, use a compact lane-label style and the row birth capsule; moving the only identity header into a conditional branch is not acceptable.
|
||||
- Check that post-fragment “unresolved” is understandable without making the diagram appear broken. Keep detail in selection help rather than repeating large warning text on every lane.
|
||||
- Current core does not export exhaustive-choice or caller lifecycle contracts. This is an explicit semantic limit, not something to guess from guard text or reference expansion.
|
||||
|
||||
No B4 release acceptance is claimed by this document. Implement only after the scenario owner yields its snapshot; then run focused projection/core tests and native checks against the exact resulting binary.
|
||||
|
||||
## Pure module implementation handoff
|
||||
|
||||
The initial pure candidate is now in `crates/oa-desktop/src/behavior/scenario_lifecycle.rs` with nested tests. It has not been wired, compiled, formatted or runtime-tested. `Geometry` borrows the existing row map `Id -> (owning sequence Id, y)` and sequence bound map `Id -> (start, end)`; no extra fragment bounds are required. `project_lifetimes` returns deterministic lane-ID ordered `LaneLifetime` records with scoped intervals, occurrence-identified birth/death markers, and conditional/repeated/parallel/missing-geometry uncertainty. Opaque ScenarioUse does not change local state or inspect callees. Call the visualization 'local occurrence lifetime' when explaining this boundary. Missing or nonfinite geometry creates uncertainty descriptors, never guessed marker positions. Root owns module declaration and Scene integration after the concurrent scenario snapshot is stable.
|
||||
Reference in New Issue
Block a user