Render explicit scenario lifetimes and preserve marker selection
This commit is contained in:
@@ -16,8 +16,6 @@ use oa_core::*;
|
||||
use std::collections::BTreeMap;
|
||||
pub(crate) mod creation;
|
||||
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;
|
||||
@@ -1092,6 +1090,7 @@ struct Scene {
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
open_calls: Vec<Point>,
|
||||
sequence_bounds: BTreeMap<Id, (f32, f32)>,
|
||||
lifecycle: scenario_lifecycle::render::Rendering,
|
||||
}
|
||||
struct SequenceSlot {
|
||||
slot: scenario::Slot,
|
||||
@@ -1150,6 +1149,7 @@ impl Scene {
|
||||
rows: BTreeMap::new(),
|
||||
open_calls: vec![],
|
||||
sequence_bounds: BTreeMap::new(),
|
||||
lifecycle: scenario_lifecycle::render::Rendering::default(),
|
||||
};
|
||||
let mut lanes = p
|
||||
.children(owner)
|
||||
@@ -1236,7 +1236,17 @@ impl Scene {
|
||||
}
|
||||
let mut y = if scenario { 148. } else { 120. };
|
||||
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0);
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
if scenario {
|
||||
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
|
||||
p,
|
||||
owner,
|
||||
&lane_x,
|
||||
&scene.rows,
|
||||
&scene.sequence_bounds,
|
||||
scene.size.height,
|
||||
);
|
||||
scene.lifecycle.align_messages(&mut scene.lines);
|
||||
let calls: Vec<_> = scene
|
||||
.rows
|
||||
.keys()
|
||||
@@ -1261,23 +1271,28 @@ impl Scene {
|
||||
scene.activation_sequence(p, p.elements[&call].parent.unwrap(), call, x, None);
|
||||
}
|
||||
}
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
for (id, name, detail) in lanes {
|
||||
let x = lane_x[&id];
|
||||
scene.lines.insert(
|
||||
0,
|
||||
Line {
|
||||
id,
|
||||
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
|
||||
label: String::new(),
|
||||
label_at: Point::ORIGIN,
|
||||
dashed: false,
|
||||
},
|
||||
);
|
||||
if !scenario {
|
||||
scene.lines.insert(
|
||||
0,
|
||||
Line {
|
||||
id,
|
||||
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
|
||||
label: String::new(),
|
||||
label_at: Point::ORIGIN,
|
||||
dashed: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
scene.boxes.push(BoxItem {
|
||||
id,
|
||||
rect: Rectangle::new(Point::new(x - 102., 15.), Size::new(204., 70.)),
|
||||
title: name,
|
||||
title: if scenario {
|
||||
format!("Lane · {name}")
|
||||
} else {
|
||||
name
|
||||
},
|
||||
detail,
|
||||
marker: None,
|
||||
});
|
||||
@@ -1319,6 +1334,16 @@ impl Scene {
|
||||
let Some(SharedBody::Occurrence { body, .. }) = e.shared() else {
|
||||
continue;
|
||||
};
|
||||
if scenario
|
||||
&& matches!(body, OccurrenceBody::Exchange {
|
||||
sender: Some(sender), receiver: Some(receiver),
|
||||
message_kind: MessageKind::Create | MessageKind::Delete, ..
|
||||
} if sender == receiver)
|
||||
{
|
||||
// Self lifecycle elbows and their caption need clearance from
|
||||
// the preceding action/state/reference box, before recording rows.
|
||||
*y += 16.;
|
||||
}
|
||||
if ordered {
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
@@ -1538,17 +1563,27 @@ impl Scene {
|
||||
});
|
||||
}
|
||||
}
|
||||
fn activation_segment(&mut self, call: Id, x: f32, top: f32, bottom: f32) {
|
||||
if bottom <= top {
|
||||
return;
|
||||
fn activation_segment(
|
||||
&mut self,
|
||||
call: Id,
|
||||
scope: Id,
|
||||
receiver: Id,
|
||||
x: f32,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
) {
|
||||
for span in self.lifecycle.alive_spans(receiver, scope, top, bottom) {
|
||||
self.boxes.push(BoxItem {
|
||||
id: call,
|
||||
rect: Rectangle::new(
|
||||
Point::new(x - 4., span.start),
|
||||
Size::new(8., span.end - span.start),
|
||||
),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
}
|
||||
self.boxes.push(BoxItem {
|
||||
id: call,
|
||||
rect: Rectangle::new(Point::new(x - 4., top), Size::new(8., bottom - top)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
}
|
||||
/// Follow a single correlation through the ordered tree. Branches are drawn
|
||||
/// as independent segments, never as one chronological stretch through ALT
|
||||
@@ -1561,6 +1596,18 @@ impl Scene {
|
||||
x: f32,
|
||||
mut start: Option<f32>,
|
||||
) -> bool {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
receiver: Some(receiver),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = p.elements[&call].shared()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let receiver = *receiver;
|
||||
let entered_open = start.is_some();
|
||||
let mut split = false;
|
||||
for id in p.elements[&owner].steps().into_iter().flatten() {
|
||||
@@ -1574,6 +1621,21 @@ impl Scene {
|
||||
continue;
|
||||
};
|
||||
match p.elements[id].shared() {
|
||||
Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Delete,
|
||||
receiver: Some(deleted),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) if *deleted == receiver => {
|
||||
// This path cannot show activation beyond its local death.
|
||||
// Keep the unresolved-call cue; a Delete is not a Reply.
|
||||
self.activation_segment(call, owner, receiver, x, top, y);
|
||||
self.open_calls.push(Point::new(x + 12., y - 16.));
|
||||
start = None;
|
||||
}
|
||||
Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
@@ -1583,14 +1645,19 @@ impl Scene {
|
||||
},
|
||||
..
|
||||
}) if *correlation == call => {
|
||||
self.activation_segment(call, x, top, y);
|
||||
self.boxes.push(BoxItem {
|
||||
id: call,
|
||||
rect: Rectangle::new(Point::new(x - 7., y - 1.), Size::new(14., 2.)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
self.activation_segment(call, owner, receiver, x, top, y);
|
||||
for span in self.lifecycle.alive_spans(receiver, owner, y - 1., y + 1.) {
|
||||
self.boxes.push(BoxItem {
|
||||
id: call,
|
||||
rect: Rectangle::new(
|
||||
Point::new(x - 7., span.start),
|
||||
Size::new(14., span.end - span.start),
|
||||
),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
}
|
||||
start = None;
|
||||
}
|
||||
Some(SharedBody::Occurrence {
|
||||
@@ -1598,7 +1665,7 @@ impl Scene {
|
||||
..
|
||||
}) => {
|
||||
split = true;
|
||||
self.activation_segment(call, x, top, y);
|
||||
self.activation_segment(call, owner, receiver, x, top, y);
|
||||
let bottom = self
|
||||
.boxes
|
||||
.iter()
|
||||
@@ -1633,7 +1700,7 @@ impl Scene {
|
||||
} else {
|
||||
top + 26.
|
||||
};
|
||||
self.activation_segment(call, x, top, bottom);
|
||||
self.activation_segment(call, owner, receiver, x, top, bottom);
|
||||
self.open_calls.push(Point::new(x + 8., bottom - 7.));
|
||||
}
|
||||
start.is_some()
|
||||
@@ -1666,6 +1733,12 @@ impl Scene {
|
||||
))
|
||||
}
|
||||
fn hit(&self, point: Point) -> Option<Id> {
|
||||
if !point.x.is_finite() || !point.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if let Some(id) = self.lifecycle.marker_at(point) {
|
||||
return Some(id);
|
||||
}
|
||||
self.boxes
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -1680,7 +1753,7 @@ impl Scene {
|
||||
.iter()
|
||||
.find(|line| {
|
||||
!line.label.is_empty()
|
||||
&& (Rectangle::new(
|
||||
&& Rectangle::new(
|
||||
line.label_at,
|
||||
Size::new(
|
||||
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
|
||||
@@ -1688,13 +1761,10 @@ impl Scene {
|
||||
),
|
||||
)
|
||||
.contains(point)
|
||||
|| line.points.windows(2).any(|p| {
|
||||
crate::diagram::distance_to_segment(point, p[0], p[1])
|
||||
< 8. / self.scale
|
||||
}))
|
||||
})
|
||||
.map(|line| line.id)
|
||||
})
|
||||
.or_else(|| self.near_sequence_item(point))
|
||||
.or_else(|| {
|
||||
self.boxes
|
||||
.iter()
|
||||
@@ -1702,6 +1772,7 @@ impl Scene {
|
||||
.find(|b| b.rect.contains(point) && b.title.is_empty())
|
||||
.map(|b| b.id)
|
||||
})
|
||||
.or_else(|| self.lifecycle.lane_at(point, self.scale))
|
||||
.or_else(|| self.lane_at(point))
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
@@ -1716,6 +1787,34 @@ impl Scene {
|
||||
.map(|line| line.id)
|
||||
})
|
||||
}
|
||||
|
||||
fn near_sequence_item(&self, point: Point) -> Option<Id> {
|
||||
if !self.scale.is_finite() || self.scale <= 0. {
|
||||
return None;
|
||||
}
|
||||
let messages = self
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|line| !line.label.is_empty())
|
||||
.map(|line| {
|
||||
let distance = line
|
||||
.points
|
||||
.windows(2)
|
||||
.map(|p| crate::diagram::distance_to_segment(point, p[0], p[1]))
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
(line.id, distance)
|
||||
});
|
||||
// One distance comparison for both kinds of screen-sized hit tolerance.
|
||||
// Dense minimum-Fit rows must not give the first message or last marker
|
||||
// a larger hit priority than the visibly nearer item. UUID breaks ties.
|
||||
self.lifecycle
|
||||
.nearest_marker(point)
|
||||
.into_iter()
|
||||
.chain(messages)
|
||||
.filter(|(_, distance)| *distance <= 8. / self.scale)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
|
||||
.map(|(id, _)| id)
|
||||
}
|
||||
}
|
||||
impl canvas::Program<Message> for Scene {
|
||||
type State = SequenceInput;
|
||||
@@ -1956,6 +2055,7 @@ impl canvas::Program<Message> for Scene {
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
f.scale(self.scale);
|
||||
self.lifecycle.draw_lanes(&mut f, self.selected);
|
||||
for b in &self.boxes {
|
||||
let outline = matches!(
|
||||
b.marker,
|
||||
@@ -2064,6 +2164,7 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
self.lifecycle.draw_markers(&mut f, self.selected);
|
||||
if let Some((context, tool, editable)) = self.interaction {
|
||||
let active = editable && state.context == Some(context) && state.tool == tool;
|
||||
let point = cursor
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
use oa_core::{FragmentOperator, Id, Kind, MessageKind, OccurrenceBody, Project, SharedBody};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub(super) mod render;
|
||||
|
||||
/// Maps already recorded by the sequence renderer. Rows carry their owning sequence.
|
||||
pub(crate) struct Geometry<'a> {
|
||||
pub rows: &'a BTreeMap<Id, (Id, f32)>,
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
//! Scene bridge for local lifetime geometry; no authoring or model mutations.
|
||||
use super::{Geometry, LaneLifetime, Life, Marker, MarkerKind, Span, project_lifetimes};
|
||||
use crate::{behavior::Line, style, views::short};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Size,
|
||||
widget::canvas::{self, Frame, Path, Stroke, Text},
|
||||
};
|
||||
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct EventMarker {
|
||||
event: Marker,
|
||||
lifeline: Id,
|
||||
center: Point,
|
||||
capsule: Rectangle,
|
||||
name: String,
|
||||
sender_x: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub(in crate::behavior) struct Rendering {
|
||||
// Complete rendering positions, including warning-admitted unassigned lanes.
|
||||
// Scene.lanes independently retains the existing authoring eligibility rule.
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
span: Option<Span>,
|
||||
lifetimes: Vec<LaneLifetime>,
|
||||
markers: Vec<EventMarker>,
|
||||
}
|
||||
|
||||
impl Rendering {
|
||||
pub(in crate::behavior) fn new(
|
||||
project: &Project,
|
||||
owner: Id,
|
||||
lanes: &BTreeMap<Id, f32>,
|
||||
rows: &BTreeMap<Id, (Id, f32)>,
|
||||
insertion_scopes: &BTreeMap<Id, (f32, f32)>,
|
||||
height: f32,
|
||||
) -> Self {
|
||||
let span = Span {
|
||||
start: 85.,
|
||||
end: height - 20.,
|
||||
};
|
||||
if !span.valid() {
|
||||
return Self::default();
|
||||
}
|
||||
// Insertion scopes deliberately start later and end earlier. Empty and
|
||||
// Create-only scenarios still have the full visible lifetime extent.
|
||||
let mut scopes = insertion_scopes.clone();
|
||||
for (scope, (start, end)) in &mut scopes {
|
||||
if *scope != owner && *start == *end && start.is_finite() {
|
||||
// An empty operand has 42 px between its insertion axis and
|
||||
// frame end. Match the existing 26 px open-activation allowance.
|
||||
*end = *start + 26.;
|
||||
}
|
||||
}
|
||||
scopes.insert(owner, (span.start, span.end));
|
||||
let lifetimes = project_lifetimes(
|
||||
project,
|
||||
owner,
|
||||
&Geometry {
|
||||
rows,
|
||||
scopes: &scopes,
|
||||
},
|
||||
);
|
||||
let lanes: BTreeMap<_, _> = lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| x.is_finite())
|
||||
.map(|(id, x)| (*id, *x))
|
||||
.collect();
|
||||
let mut markers = Vec::new();
|
||||
for lane in &lifetimes {
|
||||
let Some(&x) = lanes.get(&lane.lifeline) else {
|
||||
continue;
|
||||
};
|
||||
for event in &lane.markers {
|
||||
let sender_x = project
|
||||
.elements
|
||||
.get(&event.occurrence)
|
||||
.and_then(|e| e.shared())
|
||||
.and_then(|body| match body {
|
||||
SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
sender: Some(sender),
|
||||
..
|
||||
},
|
||||
..
|
||||
} => lanes.get(sender).copied(),
|
||||
_ => None,
|
||||
});
|
||||
markers.push(EventMarker {
|
||||
event: *event,
|
||||
lifeline: lane.lifeline,
|
||||
center: Point::new(x, event.y),
|
||||
capsule: Rectangle::new(
|
||||
Point::new(x - 70., event.y - 14.),
|
||||
Size::new(140., 28.),
|
||||
),
|
||||
name: project.named(lane.lifeline).to_owned(),
|
||||
sender_x,
|
||||
});
|
||||
}
|
||||
}
|
||||
Self {
|
||||
lanes,
|
||||
span: Some(span),
|
||||
lifetimes,
|
||||
markers,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn align_messages(&self, lines: &mut [Line]) {
|
||||
for marker in &self.markers {
|
||||
let Some(sender) = marker.sender_x else {
|
||||
continue;
|
||||
};
|
||||
let Some(line) = lines
|
||||
.iter_mut()
|
||||
.find(|line| line.id == marker.event.occurrence)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let x = marker.center.x;
|
||||
let y = marker.center.y;
|
||||
let self_message = sender == x;
|
||||
let end_x = match marker.event.kind {
|
||||
MarkerKind::Birth if sender < x => marker.capsule.x,
|
||||
MarkerKind::Birth => marker.capsule.x + marker.capsule.width,
|
||||
MarkerKind::Death => x,
|
||||
};
|
||||
line.points = if self_message {
|
||||
// The receiver is on the recorded occurrence row, including self
|
||||
// lifecycle events. The elbow stays outside the birth capsule.
|
||||
let elbow = x + 112.;
|
||||
line.label_at = Point::new(x + 12., y - 52.);
|
||||
vec![
|
||||
Point::new(x, y - 24.),
|
||||
Point::new(elbow, y - 24.),
|
||||
Point::new(elbow, y),
|
||||
Point::new(end_x, y),
|
||||
]
|
||||
} else {
|
||||
if marker.event.kind == MarkerKind::Birth {
|
||||
// Keep the label above the capsule, including right-to-left
|
||||
// messages whose existing label starts beside the receiver.
|
||||
line.label_at.y = y - 40.;
|
||||
}
|
||||
vec![Point::new(sender, y), Point::new(end_x, y)]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn alive_spans(
|
||||
&self,
|
||||
lifeline: Id,
|
||||
scope: Id,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
) -> Vec<Span> {
|
||||
if !top.is_finite() || !bottom.is_finite() || bottom <= top {
|
||||
return vec![];
|
||||
}
|
||||
self.lifetimes
|
||||
.iter()
|
||||
.find(|lane| lane.lifeline == lifeline)
|
||||
.into_iter()
|
||||
.flat_map(|lane| &lane.intervals)
|
||||
.filter(|interval| interval.scope == scope && interval.life == Life::Alive)
|
||||
.filter_map(|interval| {
|
||||
let start = top.max(interval.span.start);
|
||||
let end = bottom.min(interval.span.end);
|
||||
(start < end).then_some(Span { start, end })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn marker_at(&self, point: Point) -> Option<Id> {
|
||||
self.markers
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|marker| match marker.event.kind {
|
||||
MarkerKind::Birth => marker.capsule.contains(point),
|
||||
MarkerKind::Death => marker_distance(marker, point) <= 1.25,
|
||||
})
|
||||
.map(|marker| marker.event.occurrence)
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn nearest_marker(&self, point: Point) -> Option<(Id, f32)> {
|
||||
if !point.x.is_finite() || !point.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Exact visible shapes are tested first by Scene. A low-zoom tolerance
|
||||
// must not outrank another marker's actual capsule, a header, or a label.
|
||||
self.markers
|
||||
.iter()
|
||||
.map(|marker| (marker.event.occurrence, marker_distance(marker, point)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn lane_at(&self, point: Point, scale: f32) -> Option<Id> {
|
||||
let span = self.span?;
|
||||
if !scale.is_finite() || scale <= 0. || point.y < span.start || point.y > span.end {
|
||||
return None;
|
||||
}
|
||||
self.lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| (**x - point.x).abs() < 12. / scale)
|
||||
.min_by(|a, b| (a.1 - point.x).abs().total_cmp(&(b.1 - point.x).abs()))
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn draw_lanes(&self, frame: &mut Frame, selected: Id) {
|
||||
let Some(span) = self.span else {
|
||||
return;
|
||||
};
|
||||
for (&id, &x) in &self.lanes {
|
||||
frame.stroke(
|
||||
&Path::line(Point::new(x, span.start), Point::new(x, span.end)),
|
||||
Stroke {
|
||||
line_dash: canvas::LineDash {
|
||||
segments: &[2., 5.],
|
||||
offset: 0.,
|
||||
},
|
||||
..Stroke::default()
|
||||
.with_width(1.)
|
||||
.with_color(if id == selected {
|
||||
Color::from_rgb8(148, 190, 187)
|
||||
} else {
|
||||
Color::from_rgb8(214, 222, 222)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
for lifetime in &self.lifetimes {
|
||||
let Some(&x) = self.lanes.get(&lifetime.lifeline) else {
|
||||
continue;
|
||||
};
|
||||
for interval in &lifetime.intervals {
|
||||
match interval.life {
|
||||
Life::Alive => frame.stroke(
|
||||
&Path::line(
|
||||
Point::new(x, interval.span.start),
|
||||
Point::new(x, interval.span.end),
|
||||
),
|
||||
Stroke::default().with_width(2.).with_color(
|
||||
if lifetime.lifeline == selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
Color::from_rgb8(91, 113, 113)
|
||||
},
|
||||
),
|
||||
),
|
||||
Life::Unresolved if interval.span.end - interval.span.start >= 16. => {
|
||||
frame.fill_text(Text {
|
||||
content: "?".into(),
|
||||
position: Point::new(x + 9., interval.span.start + 2.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.lanes.is_empty() {
|
||||
frame.fill_text(Text {
|
||||
content:
|
||||
"Dotted: editing guide · Solid: local lifetime · ?: lifetime depends on path"
|
||||
.into(),
|
||||
position: Point::new(28., span.end + 4.),
|
||||
size: 10.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn draw_markers(&self, frame: &mut Frame, selected: Id) {
|
||||
for marker in &self.markers {
|
||||
let color = if selected == marker.event.occurrence || selected == marker.lifeline {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
};
|
||||
match marker.event.kind {
|
||||
MarkerKind::Birth => {
|
||||
let path = Path::rounded_rectangle(
|
||||
marker.capsule.position(),
|
||||
marker.capsule.size(),
|
||||
6.into(),
|
||||
);
|
||||
frame.fill(&path, Color::from_rgb8(243, 248, 248));
|
||||
frame.stroke(
|
||||
&path,
|
||||
Stroke::default().with_color(color).with_width(
|
||||
if selected == marker.event.occurrence {
|
||||
2.5
|
||||
} else {
|
||||
1.5
|
||||
},
|
||||
),
|
||||
);
|
||||
frame.fill_text(Text {
|
||||
content: short(&marker.name, 18),
|
||||
position: marker.capsule.position() + iced::Vector::new(8., 6.),
|
||||
size: 12.into(),
|
||||
color,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
MarkerKind::Death => {
|
||||
for (a, b) in death_strokes(marker.center) {
|
||||
frame.stroke(
|
||||
&Path::line(a, b),
|
||||
Stroke::default().with_color(color).with_width(2.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn marker_distance(marker: &EventMarker, point: Point) -> f32 {
|
||||
match marker.event.kind {
|
||||
MarkerKind::Birth => {
|
||||
let dx = (marker.capsule.x - point.x)
|
||||
.max(point.x - marker.capsule.x - marker.capsule.width)
|
||||
.max(0.);
|
||||
let dy = (marker.capsule.y - point.y)
|
||||
.max(point.y - marker.capsule.y - marker.capsule.height)
|
||||
.max(0.);
|
||||
dx.hypot(dy)
|
||||
}
|
||||
MarkerKind::Death => death_strokes(marker.center)
|
||||
.iter()
|
||||
.map(|(a, b)| crate::diagram::distance_to_segment(point, *a, *b))
|
||||
.fold(f32::INFINITY, f32::min),
|
||||
}
|
||||
}
|
||||
|
||||
fn death_strokes(center: Point) -> [(Point, Point); 2] {
|
||||
[
|
||||
(
|
||||
center + iced::Vector::new(-7., -7.),
|
||||
center + iced::Vector::new(7., 7.),
|
||||
),
|
||||
(
|
||||
center + iced::Vector::new(-7., 7.),
|
||||
center + iced::Vector::new(7., -7.),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "render_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,624 @@
|
||||
use super::*;
|
||||
use crate::{behavior::Scene, document::Document};
|
||||
use oa_core::*;
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
owner: Id,
|
||||
sender: Id,
|
||||
receiver: Id,
|
||||
work: Id,
|
||||
exchange: Id,
|
||||
}
|
||||
impl Fixture {
|
||||
fn new(layer: Layer) -> Self {
|
||||
let mut project = Project::empty();
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let a = project.add_element(kind, layer, "Request", None).unwrap();
|
||||
let work = project.add_element(kind, layer, "Provide", None).unwrap();
|
||||
let exchange = project.relate(
|
||||
if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
},
|
||||
"Service",
|
||||
a,
|
||||
work,
|
||||
);
|
||||
let owner = project
|
||||
.add_element(Kind::Scenario, layer, "Lifecycle", None)
|
||||
.unwrap();
|
||||
let mut lane = |name, represented| {
|
||||
let id = project
|
||||
.add_element(Kind::Lifeline, layer, name, Some(owner))
|
||||
.unwrap();
|
||||
*project.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(represented),
|
||||
};
|
||||
id
|
||||
};
|
||||
let sender = lane("Requester", a);
|
||||
let receiver = lane("Provider", work);
|
||||
Self {
|
||||
project,
|
||||
owner,
|
||||
sender,
|
||||
receiver,
|
||||
work,
|
||||
exchange,
|
||||
}
|
||||
}
|
||||
fn occurrence(&mut self, parent: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let mut element = self
|
||||
.project
|
||||
.make_element(
|
||||
Kind::Occurrence,
|
||||
self.project.elements[&parent].layer(),
|
||||
name,
|
||||
Some(parent),
|
||||
)
|
||||
.unwrap();
|
||||
let id = element.id;
|
||||
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
self.project.elements.insert(id, element);
|
||||
let parent = self.project.elements.get_mut(&parent).unwrap();
|
||||
if let Some(steps) = parent.steps_mut() {
|
||||
steps.push(id);
|
||||
} else if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = parent.shared_mut()
|
||||
{
|
||||
branches.push(id);
|
||||
} else {
|
||||
panic!("fixture parent must own ordered occurrences or operands");
|
||||
}
|
||||
id
|
||||
}
|
||||
fn message(&mut self, parent: Id, kind: MessageKind) -> Id {
|
||||
self.occurrence(
|
||||
parent,
|
||||
"Message",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(self.exchange),
|
||||
sender: Some(self.sender),
|
||||
receiver: Some(self.receiver),
|
||||
message_kind: kind,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn fragment(&mut self, parent: Id, operator: FragmentOperator) -> Id {
|
||||
self.occurrence(
|
||||
parent,
|
||||
"Paths",
|
||||
OccurrenceBody::Fragment {
|
||||
operator,
|
||||
branches: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn branch(&mut self, fragment: Id) -> Id {
|
||||
self.occurrence(
|
||||
fragment,
|
||||
"Operand",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "condition".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn document(&self) -> Document {
|
||||
validate_candidate(&self.project).expect("ordinary core admission must succeed");
|
||||
let mut bundle = source_project::format_bundle(&self.project);
|
||||
for text in bundle.files.values_mut() {
|
||||
*text = format!(
|
||||
"// Retain exact authored CRLF and Unicode Ω\r\n{}",
|
||||
text.replace('\n', "\r\n")
|
||||
);
|
||||
}
|
||||
let document =
|
||||
Document::from_bundle(bundle.clone(), source_project::ProjectLimits::default())
|
||||
.unwrap();
|
||||
assert_eq!(document.project(), &self.project);
|
||||
assert_eq!(document.bundle(), &bundle);
|
||||
document
|
||||
}
|
||||
fn scene(&self) -> Scene {
|
||||
let document = self.document();
|
||||
let before = document.snapshot();
|
||||
let scene = Scene::sequence(document.project(), self.owner, self.owner);
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(document.revision(), 0);
|
||||
assert!(!document.can_undo() && !document.can_redo() && !document.dirty());
|
||||
assert_eq!(
|
||||
scene.lifecycle,
|
||||
Scene::sequence(document.project(), self.owner, self.owner).lifecycle
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|line| &line.points)
|
||||
.all(|p| p.x.is_finite() && p.y.is_finite())
|
||||
);
|
||||
scene
|
||||
}
|
||||
}
|
||||
|
||||
fn intervals(scene: &Scene, lifeline: Id, scope: Id) -> Vec<(f32, f32, Life)> {
|
||||
scene
|
||||
.lifecycle
|
||||
.lifetimes
|
||||
.iter()
|
||||
.find(|lane| lane.lifeline == lifeline)
|
||||
.unwrap()
|
||||
.intervals
|
||||
.iter()
|
||||
.filter(|i| i.scope == scope)
|
||||
.map(|i| (i.span.start, i.span.end, i.life))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_empty_and_create_only_use_visible_root_extents_not_insertion_bounds() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let empty = f.scene();
|
||||
assert_eq!(empty.sequence_bounds[&f.owner], (106., 106.));
|
||||
assert_eq!(
|
||||
intervals(&empty, f.receiver, f.owner),
|
||||
vec![(85., empty.size.height - 20., Life::Alive)]
|
||||
);
|
||||
assert!(empty.lines.is_empty(), "lane guides are not message lines");
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let scene = f.scene();
|
||||
let birth = scene.rows[&create].1;
|
||||
assert_eq!(birth, 148.);
|
||||
assert_eq!(scene.sequence_bounds[&f.owner], (106., 198.));
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner),
|
||||
vec![
|
||||
(85., birth, Life::Absent),
|
||||
(birth, scene.size.height - 20., Life::Alive),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.sender, f.owner),
|
||||
vec![(85., scene.size.height - 20., Life::Alive)]
|
||||
);
|
||||
assert_eq!(scene.slots[0].y, 106.);
|
||||
assert_eq!(
|
||||
scene.lane_at(Point::new(scene.lanes[&f.receiver], 90.)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_birth_death_markers_align_endpoints_and_win_selection_without_mutating_source() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
f.occurrence(
|
||||
f.owner,
|
||||
"Work",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(f.work),
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
let mut scene = f.scene();
|
||||
let birth = scene.rows[&create].1;
|
||||
let death = scene.rows[&delete].1;
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner),
|
||||
vec![
|
||||
(85., birth, Life::Absent),
|
||||
(birth, death, Life::Alive),
|
||||
(death, scene.size.height - 20., Life::Absent),
|
||||
]
|
||||
);
|
||||
let capsule = scene
|
||||
.lifecycle
|
||||
.markers
|
||||
.iter()
|
||||
.find(|m| m.event.occurrence == create)
|
||||
.unwrap()
|
||||
.capsule;
|
||||
let line = scene.lines.iter().find(|l| l.id == create).unwrap();
|
||||
let expected_x = if scene.lanes[&f.sender] < scene.lanes[&f.receiver] {
|
||||
capsule.x
|
||||
} else {
|
||||
capsule.x + capsule.width
|
||||
};
|
||||
assert_eq!(line.points.last(), Some(&Point::new(expected_x, birth)));
|
||||
assert!(
|
||||
line.label_at.y + 22. < capsule.y,
|
||||
"message caption clears birth capsule"
|
||||
);
|
||||
let line = scene.lines.iter().find(|l| l.id == delete).unwrap();
|
||||
assert_eq!(
|
||||
line.points.last(),
|
||||
Some(&Point::new(scene.lanes[&f.receiver], death))
|
||||
);
|
||||
for scale in [0.5, 1., 2.] {
|
||||
scene.scale = scale;
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], birth)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], death)),
|
||||
Some(delete)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(
|
||||
scene.lanes[&f.receiver],
|
||||
scene.size.height - 25.
|
||||
)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], 40.)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_branch_delete_clips_only_its_activation_and_keeps_unanswered_call_visible() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment);
|
||||
let delete = f.message(left, MessageKind::Delete);
|
||||
let right = f.branch(fragment);
|
||||
let scene = f.scene();
|
||||
let death = scene.rows[&delete].1;
|
||||
let (left_start, left_end) = scene.sequence_bounds[&left];
|
||||
let (right_start, _) = scene.sequence_bounds[&right];
|
||||
let bars: Vec<_> = scene
|
||||
.boxes
|
||||
.iter()
|
||||
.filter(|b| b.id == call && b.rect.width == 8.)
|
||||
.map(|b| b.rect)
|
||||
.collect();
|
||||
assert!(
|
||||
bars.iter()
|
||||
.any(|r| r.y == left_start && r.y + r.height == death)
|
||||
);
|
||||
assert!(
|
||||
!bars
|
||||
.iter()
|
||||
.any(|r| r.y < left_end && r.y + r.height > death && r.y >= left_start)
|
||||
);
|
||||
assert!(
|
||||
bars.iter().any(|r| r.y == right_start && r.height == 26.),
|
||||
"empty exclusive sibling retains its own open activation"
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, right),
|
||||
vec![(right_start, right_start + 26., Life::Alive)]
|
||||
);
|
||||
let frame_end = scene.boxes.iter().find(|b| b.id == fragment).unwrap().rect;
|
||||
assert!(
|
||||
!bars.iter().any(|r| r.y >= frame_end.y + frame_end.height),
|
||||
"uncertain post-fragment lifetime cannot carry a definite activation bar"
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.open_calls
|
||||
.iter()
|
||||
.any(|p| p.y < death && p.y > left_start)
|
||||
);
|
||||
assert!(scene.open_calls.iter().any(|p| p.y > right_start));
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, left),
|
||||
vec![
|
||||
(left_start, death, Life::Alive),
|
||||
(death, left_end, Life::Absent)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner).last().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert!(!f.project.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Reply,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_unassigned_lanes_and_explicit_incomplete_receiver_keep_real_marker_identity() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
*f.project
|
||||
.elements
|
||||
.get_mut(&f.receiver)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Lifeline { represented: None };
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = f.project.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
}
|
||||
let scene = f.scene();
|
||||
assert!(
|
||||
!scene.lanes.contains_key(&f.receiver),
|
||||
"authoring eligibility remains separate"
|
||||
);
|
||||
let x = scene.lifecycle.lanes[&f.receiver];
|
||||
assert_eq!(scene.lifecycle.markers[0].lifeline, f.receiver);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&create].1)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(scene.hit(Point::new(x, 40.)), Some(f.receiver));
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.size.height - 25.)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
assert_eq!(scene.lane_at(Point::new(x, 90.)), None);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
receiver, exchange, ..
|
||||
},
|
||||
..
|
||||
}) = f.project.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*receiver = None;
|
||||
*exchange = Some(f.exchange);
|
||||
}
|
||||
let missing = f.scene();
|
||||
assert!(
|
||||
missing.lifecycle.markers.is_empty(),
|
||||
"do not infer receiver from global exchange"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_minimum_zoom_marker_halos_do_not_steal_adjacent_capsule_or_header_hits() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
*f.project
|
||||
.elements
|
||||
.get_mut(&f.receiver)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Lifeline { represented: None };
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
for event in [create, delete] {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = f.project.elements.get_mut(&event).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
}
|
||||
}
|
||||
let mut scene = f.scene();
|
||||
let x = scene.lifecycle.lanes[&f.receiver];
|
||||
assert_eq!(scene.rows[&delete].1 - scene.rows[&create].1, 92.);
|
||||
for scale in [0.05, 0.1, 0.5, 1., 2.] {
|
||||
scene.scale = scale;
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&create].1)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&delete].1)),
|
||||
Some(delete)
|
||||
);
|
||||
assert_eq!(scene.hit(Point::new(x, 40.)), Some(f.receiver));
|
||||
if scale <= 0.1 {
|
||||
for offset in [-25., -15., 15., 25.] {
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&delete].1 + offset)),
|
||||
Some(delete),
|
||||
"a nearby death halo must beat an earlier message's wider tolerance"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_minimum_zoom_near_ordinary_message_wins_over_lifecycle_halos() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
f.message(f.owner, MessageKind::Create);
|
||||
let ordinary = f.message(f.owner, MessageKind::Asynchronous);
|
||||
f.message(f.owner, MessageKind::Delete);
|
||||
let mut scene = f.scene();
|
||||
let x = (scene.lanes[&f.sender] + scene.lanes[&f.receiver]) / 2.;
|
||||
for scale in [0.05, 0.1] {
|
||||
scene.scale = scale;
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&ordinary].1 + 2.)),
|
||||
Some(ordinary)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_self_lifecycle_elbows_end_on_recorded_marker_rows() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for kind in [MessageKind::Create, MessageKind::Delete] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let event = f.message(f.owner, kind);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
exchange, sender, ..
|
||||
},
|
||||
..
|
||||
}) = f.project.elements.get_mut(&event).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
*sender = Some(f.receiver);
|
||||
}
|
||||
let scene = f.scene();
|
||||
let marker = &scene.lifecycle.markers[0];
|
||||
let line = scene.lines.iter().find(|l| l.id == event).unwrap();
|
||||
assert_eq!(line.points.len(), 4);
|
||||
assert_eq!(line.points[0].y, marker.center.y - 24.);
|
||||
assert_eq!(line.points[3].y, marker.center.y);
|
||||
assert_eq!(
|
||||
line.points[3].x,
|
||||
if marker.event.kind == MarkerKind::Birth {
|
||||
marker.capsule.x + marker.capsule.width
|
||||
} else {
|
||||
marker.center.x
|
||||
}
|
||||
);
|
||||
assert!(line.points[1].x > marker.capsule.x + marker.capsule.width);
|
||||
assert_eq!(scene.hit(marker.center), Some(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_self_delete_caption_clears_the_preceding_action_box() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let action = f.occurrence(
|
||||
f.owner,
|
||||
"Finish work",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(f.work),
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange, sender, ..
|
||||
},
|
||||
..
|
||||
}) = f.project.elements.get_mut(&delete).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
*sender = Some(f.receiver);
|
||||
}
|
||||
let scene = f.scene();
|
||||
let action = scene.boxes.iter().find(|b| b.id == action).unwrap();
|
||||
let line = scene.lines.iter().find(|l| l.id == delete).unwrap();
|
||||
assert!(line.label_at.y > action.rect.y + action.rect.height);
|
||||
assert!(line.label_at.y + 22. < line.points[0].y);
|
||||
assert_eq!(line.points[3].y, scene.rows[&delete].1);
|
||||
assert_eq!(
|
||||
scene
|
||||
.slots
|
||||
.iter()
|
||||
.find(|s| s.slot.before == Some(delete))
|
||||
.unwrap()
|
||||
.y,
|
||||
scene.rows[&delete].1 - 42.
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_conditional_repeated_and_parallel_scopes_do_not_claim_definite_continuation() {
|
||||
for operator in [
|
||||
FragmentOperator::Alternative,
|
||||
FragmentOperator::Optional,
|
||||
FragmentOperator::Loop,
|
||||
FragmentOperator::Parallel,
|
||||
] {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let fragment = f.fragment(f.owner, operator);
|
||||
let first = f.branch(fragment);
|
||||
let create = f.message(first, MessageKind::Create);
|
||||
let scene = f.scene();
|
||||
assert_eq!(scene.lifecycle.markers[0].event.occurrence, create);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner).first().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner).last().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.lifecycle
|
||||
.lifetimes
|
||||
.iter()
|
||||
.find(|l| l.lifeline == f.sender)
|
||||
.unwrap()
|
||||
.intervals
|
||||
.iter()
|
||||
.all(|i| i.life == Life::Alive)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_lifecycle_edit_undo_redo_restores_exact_source_and_changed_geometry() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let mut document = f.document();
|
||||
let before = document.snapshot();
|
||||
let geometry = Scene::sequence(document.project(), f.owner, create).lifecycle;
|
||||
let mut edited = document.project().elements[&create].clone();
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||
..
|
||||
}) = edited.shared_mut()
|
||||
{
|
||||
*message_kind = MessageKind::Asynchronous;
|
||||
}
|
||||
document
|
||||
.execute(Command::EditElement(create, edited))
|
||||
.unwrap();
|
||||
let after = document.snapshot();
|
||||
let changed = Scene::sequence(document.project(), f.owner, create).lifecycle;
|
||||
assert_ne!(geometry, changed);
|
||||
assert!(changed.markers.is_empty());
|
||||
assert_ne!(before.bundle, after.bundle);
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(
|
||||
Scene::sequence(document.project(), f.owner, create).lifecycle,
|
||||
geometry
|
||||
);
|
||||
assert!(!document.can_undo() && document.can_redo());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), &after.decoded.project);
|
||||
assert_eq!(document.bundle(), &after.bundle);
|
||||
assert_eq!(
|
||||
Scene::sequence(document.project(), f.owner, create).lifecycle,
|
||||
changed
|
||||
);
|
||||
assert!(!document.can_redo());
|
||||
}
|
||||
}
|
||||
@@ -484,7 +484,7 @@ fn article(topic: Topic) -> Article {
|
||||
const {
|
||||
Article {
|
||||
summary: "Use a scenario to tell a concrete behavioral story, including repeated messages, actions, state observations and guarded alternatives.",
|
||||
keywords: "scenario sequence lifeline sender receiver fragment branch alternative loop guard parallel strict optional occurrence exchange action state interface operation allocation call reply unresolved",
|
||||
keywords: "scenario sequence lifeline lifetime lifecycle birth death capsule sender receiver fragment branch alternative loop guard parallel strict optional occurrence exchange action state interface operation allocation call reply unresolved",
|
||||
sections: &[
|
||||
walkthrough(
|
||||
"Create a scenario",
|
||||
@@ -506,6 +506,14 @@ fn article(topic: Topic) -> Article {
|
||||
"Inspect the scenario projection and Model checks. State exactly what each guard means, including units and assumptions where relevant.",
|
||||
],
|
||||
),
|
||||
explain(
|
||||
"Read lifeline creation and deletion",
|
||||
"The Lane header and dotted guide identify a lifeline; the guide alone does not establish its lifetime. The stronger solid line shows where the lifeline is known to exist on this scenario path. Create starts that line at a named capsule; Delete ends it at an X. Select the capsule or X to edit that message occurrence. Select the header or guide to inspect the lifeline.",
|
||||
),
|
||||
explain(
|
||||
"Distinguish uncertain lifetime from an unanswered call",
|
||||
"A ? means lifetime depends on an alternative, optional, repeated or parallel path. Operands have independent lifetimes: Delete in one alternative does not delete a lifeline in another. Beyond such a fragment, the guide can continue without a definite solid line. This uncertainty is not by itself a model error. The … beside an activation marks a Call that remains unanswered on that path. A reused scenario's internal Create/Delete events appear when you choose Open definition; they do not create or delete the caller's lifelines.",
|
||||
),
|
||||
walkthrough(
|
||||
"Invoke an interface operation",
|
||||
&[
|
||||
|
||||
+87
-21
@@ -752,7 +752,7 @@ lists all additional fields. <span class="docutils literal">ref</span> means <sp
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_scenario</span>, <span class="docutils literal">sa_scenario</span></p></td>
|
||||
<td><p><span class="docutils literal">viewpoint</span>: ScenarioViewpoint; <span class="docutils literal">context</span>: ref?; <span class="docutils literal">precondition</span>, <span class="docutils literal">postcondition</span>: strings; <span class="docutils literal">steps</span>: occurrence refs</p></td>
|
||||
<td><p>MixedLegacy for omitted schema-2 source; none; <span class="docutils literal">""</span>; <span class="docutils literal">""</span>; <span class="docutils literal">[]</span></p></td>
|
||||
<td><p>MixedLegacy when viewpoint is omitted, including newly authored source 7 (see section 4.3); none; <span class="docutils literal">""</span>; <span class="docutils literal">""</span>; <span class="docutils literal">[]</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_lifeline</span>, <span class="docutils literal">sa_lifeline</span></p></td>
|
||||
<td><p><span class="docutils literal">represented</span>: participant, work, or role ref?</p></td>
|
||||
@@ -792,49 +792,71 @@ An Enumeration contains unique string literals. For example:</p>
|
||||
<pre class="code text literal-block"><code>definition { Enumeration { literals ["Clear", "Fog"]; }; };
|
||||
type_ref { Named @weather_classification; };</code></pre>
|
||||
<p>These are typed constructors with named fields, not embedded JSON documents.
|
||||
Constructor field names and enum spellings are case-sensitive. When replacing
|
||||
a constructor, all of its fields MUST be supplied; the containing declaration
|
||||
defaults do not silently fill a different constructor's missing fields.
|
||||
During schema-2 compatibility, the new Exchange fields <span class="docutils literal">message_kind</span>,
|
||||
<span class="docutils literal">reply_to</span> and <span class="docutils literal">values</span> are exceptions: omitted values hydrate as
|
||||
Asynchronous, none and [] respectively. Canonical output writes them explicitly.
|
||||
This preserves older schema-2 source/JSON without inventing call correlation or
|
||||
value annotations. It is not a rule for an unspecified future schema.</p>
|
||||
Constructor field names and enum spellings are case-sensitive. A selected
|
||||
constructor uses its own field contract; it does not inherit the payload of a
|
||||
different constructor from the declaration default. Required fields MUST be
|
||||
supplied. Nullability and omission are separate properties: <span class="docutils literal">?</span> denotes a
|
||||
value that can be <span class="docutils literal">none</span>; a constructor's omission contract determines whether
|
||||
the field may be absent. There is no general all-constructor-fields-required
|
||||
rule. Other typed constructors use their contracts in the corresponding
|
||||
sections; the occurrence rules below do not establish defaults for them.</p>
|
||||
<p>The following table records the source-7 OccurrenceBody contracts. Optional
|
||||
references may be omitted and then mean <span class="docutils literal">none</span>. Exchange also admits omitted
|
||||
<span class="docutils literal">message_kind</span> and <span class="docutils literal">values</span>, meaning Asynchronous and <span class="docutils literal">[]</span> respectively.
|
||||
These omissions are accepted in newly authored source 7, not only in schema-2
|
||||
compatibility input. Earlier schema admission of constructors and fields is
|
||||
still governed by its version rules.</p>
|
||||
<p>Canonical formatting writes the hydrated occurrence references and default
|
||||
<span class="docutils literal">message_kind</span>, <span class="docutils literal">reply_to</span> and <span class="docutils literal">values</span> explicitly. An absent
|
||||
<span class="docutils literal">operation</span> remains omitted in canonical output. Lossless no-op updates and
|
||||
unrelated name edits retain authored omissions; canonical formatting is a
|
||||
different operation. An admitted incomplete occurrence can still produce model
|
||||
checks requesting its missing work, exchange, lifeline or referenced scenario.</p>
|
||||
<table>
|
||||
<caption>Occurrence body constructors</caption>
|
||||
<colgroup>
|
||||
<col style="width: 22.0%" />
|
||||
<col style="width: 78.0%" />
|
||||
<col style="width: 18.0%" />
|
||||
<col style="width: 36.0%" />
|
||||
<col style="width: 46.0%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr><th class="head"><p>Constructor</p></th>
|
||||
<th class="head"><p>Required fields</p></th>
|
||||
<th class="head"><p>Fields that may be omitted, with their resulting values</p></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><p>Exchange</p></td>
|
||||
<td><p><span class="docutils literal">exchange</span>: exchange relation ref?; <span class="docutils literal">sender</span>, <span class="docutils literal">receiver</span>: lifeline refs?; <span class="docutils literal">message_kind</span>: MessageKind; <span class="docutils literal">reply_to</span>: occurrence ref?; <span class="docutils literal">values</span>: MessageValue records</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">exchange</span>: exchange relation ref?, <span class="docutils literal">sender</span>, <span class="docutils literal">receiver</span>: lifeline refs?, <span class="docutils literal">reply_to</span>: occurrence ref?, <span class="docutils literal">operation</span>: OperationUse? → <span class="docutils literal">none</span>; <span class="docutils literal">message_kind</span>: MessageKind → Asynchronous; <span class="docutils literal">values</span>: MessageValue records → <span class="docutils literal">[]</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p>Action</p></td>
|
||||
<td><p><span class="docutils literal">action</span>: activity/function ref?; <span class="docutils literal">lifeline</span>: ref?</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">action</span>: activity/function ref?, <span class="docutils literal">lifeline</span>: ref? → <span class="docutils literal">none</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p>Control</p></td>
|
||||
<td><p><span class="docutils literal">kind</span>: Choice, Merge, Fork, Join, Loop</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
<tr><td><p>SequenceReference</p></td>
|
||||
<td><p><span class="docutils literal">sequence</span>: process/functional-chain ref?</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">sequence</span>: process/functional-chain ref? → <span class="docutils literal">none</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p>ScenarioUse</p></td>
|
||||
<td><p><span class="docutils literal">scenario</span>: scenario ref?; <span class="docutils literal">bindings</span>: LifelineBinding records</p></td>
|
||||
<td><p><span class="docutils literal">bindings</span>: LifelineBinding records</p></td>
|
||||
<td><p><span class="docutils literal">scenario</span>: scenario ref? → <span class="docutils literal">none</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p>State</p></td>
|
||||
<td><p><span class="docutils literal">state</span>: state/mode ref?; <span class="docutils literal">lifeline</span>: ref?</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">state</span>: state/mode ref?, <span class="docutils literal">lifeline</span>: ref? → <span class="docutils literal">none</span></p></td>
|
||||
</tr>
|
||||
<tr><td><p>Fragment</p></td>
|
||||
<td><p><span class="docutils literal">operator</span>: Alternative, Parallel, Loop, Optional; <span class="docutils literal">branches</span>: branch occurrence refs</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Branch</p></td>
|
||||
<td><p><span class="docutils literal">guard</span>: string; <span class="docutils literal">occurrences</span>: occurrence refs</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1297,16 +1319,25 @@ references a Call occurrence in the same scenario. The call MUST precede the
|
||||
reply on a guaranteed common sequence/enclosing branch path. Reply lifelines
|
||||
reverse the call's sender/receiver; it may reuse the call's exchange in reverse
|
||||
or use an explicit reverse-direction exchange. A second reply to the same call
|
||||
on that guaranteed path is an error. Missing reply correlation is a draft
|
||||
that can follow another reply on a shared execution path, including conditional
|
||||
or parallel operands, is an error. Missing reply correlation is a draft
|
||||
warning; non-Reply messages MUST NOT have <span class="docutils literal">reply_to</span>.</p>
|
||||
<p>Create identifies the receiver lifeline's creation. An earlier guaranteed use
|
||||
of that lifeline is an error. Delete identifies its destruction; a later
|
||||
guaranteed use is an error. Ordering checks respect branch ownership; they MUST
|
||||
NOT flatten alternative branches into a false unconditional order. A Timer is
|
||||
<p>Create identifies the receiver lifeline's creation. Another use that can precede
|
||||
its creation on a shared execution path is an error. Delete identifies its
|
||||
destruction; another use that can follow it on a shared execution path is an
|
||||
error. These checks include conditional and parallel operands but exclude
|
||||
mutually exclusive alternatives. Ordering checks respect branch ownership; they
|
||||
MUST NOT flatten alternative branches into a false unconditional order. A Timer is
|
||||
local to its sender lifeline, has no global exchange or values, and has either
|
||||
no receiver or the same receiver as sender. Its nonempty expression describes
|
||||
the delay/deadline; an empty expression warns. Conditions and expressions are
|
||||
stored engineering annotations, not an executable simulation language.</p>
|
||||
<p>These ordering checks compare distinct stored occurrences along structural
|
||||
paths; they do not simulate repeated loop iterations. Repetition risks use
|
||||
<span class="docutils literal">reply_repetition</span> and <span class="docutils literal">lifeline_repetition</span> warnings separately. For example,
|
||||
a loop containing one Reply to an outer Call can warn about repetition without
|
||||
containing two distinct replies; a same-operand Create/use/Delete lifetime can
|
||||
repeat without its previous iteration being treated as an earlier stored use.</p>
|
||||
<p>Each MessageValue is <span class="docutils literal">{ path [@parameter]; field @data_field; expression "value expression"; }</span>.
|
||||
Its identity is the pair (path, field), so two uses of one Record can have
|
||||
different values for the same terminal field. Path contains enclosing DataField
|
||||
@@ -2618,6 +2649,41 @@ leaving dangling references. Deleting a sequence does not delete the semantic
|
||||
exchanges it references. Cross-layer counterparts are not owned descendants.
|
||||
Text edits describe a complete candidate and must remove or repair dependencies
|
||||
explicitly before Apply succeeds.</p>
|
||||
<section id="scenario-lifetime-presentation">
|
||||
<h3>8.1. Scenario lifetime presentation</h3>
|
||||
<p>Scenario lifetime geometry is derived from the stored occurrences and their
|
||||
sequence/operand ownership. It adds no model records, source annotations or
|
||||
history entries. A lifeline's identity header and neutral editing guide remain
|
||||
available before creation, after deletion and when its represented element is
|
||||
unfinished. The guide MUST be visually distinguishable from a live interval;
|
||||
it is an editing axis, not a statement that the participant exists.</p>
|
||||
<p>A directly ordered Create starts the receiver's live interval at its occurrence;
|
||||
Delete ends that receiver's interval at its occurrence. Create uses a named birth
|
||||
marker and Delete an X, each selectable through the original occurrence identity.
|
||||
The message's receiving endpoint MUST align with its marker, including self
|
||||
messages. A pre-existing lifeline without a creation has a live interval from
|
||||
below its header; without deletion it extends to the visible scenario bottom,
|
||||
including in an empty scenario. A missing lifecycle receiver MUST NOT be inferred from
|
||||
the exchange. An explicit local receiver remains meaningful when its exchange
|
||||
reference is unfinished.</p>
|
||||
<p>Operands MUST be projected independently from their enclosing entry context.
|
||||
A lifetime change in an alternative, optional, loop or parallel operand MUST
|
||||
NOT imply the same change in its displayed sibling or a definite lifetime
|
||||
after the fragment. When local structure does not establish one lifetime across
|
||||
the represented paths, the continuation is visually unresolved. A subsequent
|
||||
explicit Create or Delete in the enclosing sequence can establish a definite interval.
|
||||
Neither guard text, screen order nor a guessed loop count proves execution.
|
||||
ScenarioUse remains opaque: opening its definition may show its lifecycle,
|
||||
but a callee's geometry MUST NOT create or destroy a caller lifeline.</p>
|
||||
<p>Call activation is distinct from lifetime. A branch-local Delete clips that
|
||||
branch's visible activation at the death marker without inventing a Reply,
|
||||
removing an unresolved-call warning or terminating sibling paths.
|
||||
Solid activation bars are restricted to definite live intervals; an unresolved
|
||||
continuation retains its uncertainty/call diagnostic rather than depicting
|
||||
definite survival after a possible deletion. Selection,
|
||||
insertion slots, source edits and Undo/Redo retain the same semantic identities
|
||||
and validation boundary regardless of the rendered intervals.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="persistence-and-explicit-migration">
|
||||
<h2>9. Persistence and explicit migration</h2>
|
||||
|
||||
+92
-20
@@ -120,7 +120,7 @@ lists all additional fields. ``ref`` means ``@alias``; ``ref?`` also accepts
|
||||
- No implicit contract; ``legacy`` defaults none. OA admits Legacy only.
|
||||
* - ``oa_scenario``, ``sa_scenario``
|
||||
- ``viewpoint``: ScenarioViewpoint; ``context``: ref?; ``precondition``, ``postcondition``: strings; ``steps``: occurrence refs
|
||||
- MixedLegacy for omitted schema-2 source; none; ``""``; ``""``; ``[]``
|
||||
- MixedLegacy when viewpoint is omitted, including newly authored source 7 (see section 4.3); none; ``""``; ``""``; ``[]``
|
||||
* - ``oa_lifeline``, ``sa_lifeline``
|
||||
- ``represented``: participant, work, or role ref?
|
||||
- none
|
||||
@@ -155,37 +155,60 @@ An Enumeration contains unique string literals. For example:
|
||||
type_ref { Named @weather_classification; };
|
||||
|
||||
These are typed constructors with named fields, not embedded JSON documents.
|
||||
Constructor field names and enum spellings are case-sensitive. When replacing
|
||||
a constructor, all of its fields MUST be supplied; the containing declaration
|
||||
defaults do not silently fill a different constructor's missing fields.
|
||||
During schema-2 compatibility, the new Exchange fields ``message_kind``,
|
||||
``reply_to`` and ``values`` are exceptions: omitted values hydrate as
|
||||
Asynchronous, none and [] respectively. Canonical output writes them explicitly.
|
||||
This preserves older schema-2 source/JSON without inventing call correlation or
|
||||
value annotations. It is not a rule for an unspecified future schema.
|
||||
Constructor field names and enum spellings are case-sensitive. A selected
|
||||
constructor uses its own field contract; it does not inherit the payload of a
|
||||
different constructor from the declaration default. Required fields MUST be
|
||||
supplied. Nullability and omission are separate properties: ``?`` denotes a
|
||||
value that can be ``none``; a constructor's omission contract determines whether
|
||||
the field may be absent. There is no general all-constructor-fields-required
|
||||
rule. Other typed constructors use their contracts in the corresponding
|
||||
sections; the occurrence rules below do not establish defaults for them.
|
||||
|
||||
The following table records the source-7 OccurrenceBody contracts. Optional
|
||||
references may be omitted and then mean ``none``. Exchange also admits omitted
|
||||
``message_kind`` and ``values``, meaning Asynchronous and ``[]`` respectively.
|
||||
These omissions are accepted in newly authored source 7, not only in schema-2
|
||||
compatibility input. Earlier schema admission of constructors and fields is
|
||||
still governed by its version rules.
|
||||
|
||||
Canonical formatting writes the hydrated occurrence references and default
|
||||
``message_kind``, ``reply_to`` and ``values`` explicitly. An absent
|
||||
``operation`` remains omitted in canonical output. Lossless no-op updates and
|
||||
unrelated name edits retain authored omissions; canonical formatting is a
|
||||
different operation. An admitted incomplete occurrence can still produce model
|
||||
checks requesting its missing work, exchange, lifeline or referenced scenario.
|
||||
|
||||
.. list-table:: Occurrence body constructors
|
||||
:header-rows: 1
|
||||
:widths: 22 78
|
||||
:widths: 18 36 46
|
||||
|
||||
* - Constructor
|
||||
- Required fields
|
||||
- Fields that may be omitted, with their resulting values
|
||||
* - Exchange
|
||||
- ``exchange``: exchange relation ref?; ``sender``, ``receiver``: lifeline refs?; ``message_kind``: MessageKind; ``reply_to``: occurrence ref?; ``values``: MessageValue records
|
||||
- None
|
||||
- ``exchange``: exchange relation ref?, ``sender``, ``receiver``: lifeline refs?, ``reply_to``: occurrence ref?, ``operation``: OperationUse? → ``none``; ``message_kind``: MessageKind → Asynchronous; ``values``: MessageValue records → ``[]``
|
||||
* - Action
|
||||
- ``action``: activity/function ref?; ``lifeline``: ref?
|
||||
- None
|
||||
- ``action``: activity/function ref?, ``lifeline``: ref? → ``none``
|
||||
* - Control
|
||||
- ``kind``: Choice, Merge, Fork, Join, Loop
|
||||
- None
|
||||
* - SequenceReference
|
||||
- ``sequence``: process/functional-chain ref?
|
||||
- None
|
||||
- ``sequence``: process/functional-chain ref? → ``none``
|
||||
* - ScenarioUse
|
||||
- ``scenario``: scenario ref?; ``bindings``: LifelineBinding records
|
||||
- ``bindings``: LifelineBinding records
|
||||
- ``scenario``: scenario ref? → ``none``
|
||||
* - State
|
||||
- ``state``: state/mode ref?; ``lifeline``: ref?
|
||||
- None
|
||||
- ``state``: state/mode ref?, ``lifeline``: ref? → ``none``
|
||||
* - Fragment
|
||||
- ``operator``: Alternative, Parallel, Loop, Optional; ``branches``: branch occurrence refs
|
||||
- None
|
||||
* - Branch
|
||||
- ``guard``: string; ``occurrences``: occurrence refs
|
||||
- None
|
||||
|
||||
For example, ``body { Action { action @assess; lifeline @operator_line; }; };``
|
||||
selects an Action occurrence. Processes and functional chains contain Exchange,
|
||||
@@ -657,18 +680,28 @@ references a Call occurrence in the same scenario. The call MUST precede the
|
||||
reply on a guaranteed common sequence/enclosing branch path. Reply lifelines
|
||||
reverse the call's sender/receiver; it may reuse the call's exchange in reverse
|
||||
or use an explicit reverse-direction exchange. A second reply to the same call
|
||||
on that guaranteed path is an error. Missing reply correlation is a draft
|
||||
that can follow another reply on a shared execution path, including conditional
|
||||
or parallel operands, is an error. Missing reply correlation is a draft
|
||||
warning; non-Reply messages MUST NOT have ``reply_to``.
|
||||
|
||||
Create identifies the receiver lifeline's creation. An earlier guaranteed use
|
||||
of that lifeline is an error. Delete identifies its destruction; a later
|
||||
guaranteed use is an error. Ordering checks respect branch ownership; they MUST
|
||||
NOT flatten alternative branches into a false unconditional order. A Timer is
|
||||
Create identifies the receiver lifeline's creation. Another use that can precede
|
||||
its creation on a shared execution path is an error. Delete identifies its
|
||||
destruction; another use that can follow it on a shared execution path is an
|
||||
error. These checks include conditional and parallel operands but exclude
|
||||
mutually exclusive alternatives. Ordering checks respect branch ownership; they
|
||||
MUST NOT flatten alternative branches into a false unconditional order. A Timer is
|
||||
local to its sender lifeline, has no global exchange or values, and has either
|
||||
no receiver or the same receiver as sender. Its nonempty expression describes
|
||||
the delay/deadline; an empty expression warns. Conditions and expressions are
|
||||
stored engineering annotations, not an executable simulation language.
|
||||
|
||||
These ordering checks compare distinct stored occurrences along structural
|
||||
paths; they do not simulate repeated loop iterations. Repetition risks use
|
||||
``reply_repetition`` and ``lifeline_repetition`` warnings separately. For example,
|
||||
a loop containing one Reply to an outer Call can warn about repetition without
|
||||
containing two distinct replies; a same-operand Create/use/Delete lifetime can
|
||||
repeat without its previous iteration being treated as an earlier stored use.
|
||||
|
||||
Each MessageValue is ``{ path [@parameter]; field @data_field; expression "value expression"; }``.
|
||||
Its identity is the pair (path, field), so two uses of one Record can have
|
||||
different values for the same terminal field. Path contains enclosing DataField
|
||||
@@ -2039,6 +2072,45 @@ exchanges it references. Cross-layer counterparts are not owned descendants.
|
||||
Text edits describe a complete candidate and must remove or repair dependencies
|
||||
explicitly before Apply succeeds.
|
||||
|
||||
8.1. Scenario lifetime presentation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Scenario lifetime geometry is derived from the stored occurrences and their
|
||||
sequence/operand ownership. It adds no model records, source annotations or
|
||||
history entries. A lifeline's identity header and neutral editing guide remain
|
||||
available before creation, after deletion and when its represented element is
|
||||
unfinished. The guide MUST be visually distinguishable from a live interval;
|
||||
it is an editing axis, not a statement that the participant exists.
|
||||
|
||||
A directly ordered Create starts the receiver's live interval at its occurrence;
|
||||
Delete ends that receiver's interval at its occurrence. Create uses a named birth
|
||||
marker and Delete an X, each selectable through the original occurrence identity.
|
||||
The message's receiving endpoint MUST align with its marker, including self
|
||||
messages. A pre-existing lifeline without a creation has a live interval from
|
||||
below its header; without deletion it extends to the visible scenario bottom,
|
||||
including in an empty scenario. A missing lifecycle receiver MUST NOT be inferred from
|
||||
the exchange. An explicit local receiver remains meaningful when its exchange
|
||||
reference is unfinished.
|
||||
|
||||
Operands MUST be projected independently from their enclosing entry context.
|
||||
A lifetime change in an alternative, optional, loop or parallel operand MUST
|
||||
NOT imply the same change in its displayed sibling or a definite lifetime
|
||||
after the fragment. When local structure does not establish one lifetime across
|
||||
the represented paths, the continuation is visually unresolved. A subsequent
|
||||
explicit Create or Delete in the enclosing sequence can establish a definite interval.
|
||||
Neither guard text, screen order nor a guessed loop count proves execution.
|
||||
ScenarioUse remains opaque: opening its definition may show its lifecycle,
|
||||
but a callee's geometry MUST NOT create or destroy a caller lifeline.
|
||||
|
||||
Call activation is distinct from lifetime. A branch-local Delete clips that
|
||||
branch's visible activation at the death marker without inventing a Reply,
|
||||
removing an unresolved-call warning or terminating sibling paths.
|
||||
Solid activation bars are restricted to definite live intervals; an unresolved
|
||||
continuation retains its uncertainty/call diagnostic rather than depicting
|
||||
definite survival after a possible deletion. Selection,
|
||||
insertion slots, source edits and Undo/Redo retain the same semantic identities
|
||||
and validation boundary regardless of the rendered intervals.
|
||||
|
||||
9. Persistence and explicit migration
|
||||
-------------------------------------
|
||||
|
||||
|
||||
@@ -319,3 +319,30 @@ Revisit if complete text/native tasks expose unacceptable hidden propagation,
|
||||
wrong-context edits or inability to preserve reproducible independent studies.
|
||||
The comparison establishes semantic/text maintenance evidence, not human task
|
||||
timings or full quantity/library integration acceptance.
|
||||
|
||||
## Scenario lifetime rendering integration — 2026-09-11
|
||||
|
||||
Codex-ytr83r /root. Adopt the [scoped lifecycle proposal](release-scenario-lifecycle-design-20260910.md)
|
||||
as derived presentation over existing source-7 semantics. Always retain a neutral
|
||||
identity header/editing guide; depict definite live intervals separately, with
|
||||
original-occurrence Create capsules and Delete crosses. Alternative, optional,
|
||||
loop and parallel operands inherit their enclosing entry independently and leave
|
||||
an unresolved continuation after a local lifetime change. Reused scenarios remain
|
||||
opaque. This does not execute guards or add model/source/history records.
|
||||
|
||||
Full-height ordinary spines fail to communicate creation/deletion; one global
|
||||
earliest-birth/latest-death range would falsely order exclusive operands. Both
|
||||
alternatives lose to the scoped projection. The [independent review](release-scenario-lifecycle-independent-review-20260911.md)
|
||||
requires true visible root bounds, a complete rendering lane map and local
|
||||
activation clipping at Delete; insertion bounds/eligible lanes must keep their
|
||||
separate roles. Receiver endpoints and selectable markers must agree, including
|
||||
self messages. Those findings determine the integration contract in language
|
||||
section 8.1 and the real-Scene/native acceptance fixtures.
|
||||
|
||||
Core/source semantics are unchanged. In section 4.3, replacing the old
|
||||
guaranteed-only lifetime/duplicate-reply wording with possible shared-path wording
|
||||
corrects documentation to `validation.rs` and the existing conditional/parallel
|
||||
regressions; it does not broaden admission or add a new rule. Compilation and
|
||||
native acceptance of the renderer are still pending at this decision. Revisit the
|
||||
visual encoding if actual minimum-size engineering workflows make marker identity,
|
||||
uncertain continuation or the distinction between guide and existence unclear.
|
||||
|
||||
+44
-11
@@ -15,12 +15,43 @@ models must be practical to author entirely in uncluttered text.
|
||||
|
||||
Root verified and imported three exact bundles. Behavior `c6d296a` is integrated
|
||||
at `c81e285`, preserving the latest user-provided goal bytes. Creation `4bb4aaf`
|
||||
is being integrated as its single coherent commit; prior workspace `3978bbe`
|
||||
is integrated at `e207b04` as its single coherent commit; prior workspace `3978bbe`
|
||||
and Sensor `d114a27` already have equivalents in behavior ancestry.
|
||||
[Independent integration audit](release-integration-ytr83r-20260911.md) records
|
||||
exact bundle hashes, source overlap and preservation rules. Transport branches
|
||||
are delivery containers, not branches to merge into production history.
|
||||
|
||||
## Current local execution — 2026-09-11
|
||||
|
||||
Evidence `2ec72d4` is selectively imported at `b853a1e`, including corrected
|
||||
creation-oracle v2/native records and constructor-default probes. The complete
|
||||
import preflight found 132 new/58 exact/3 reviewed replacement files; local ledgers
|
||||
were preserved. The newer project AGENTS release-milestone instruction is imported.
|
||||
|
||||
Rust1.98.1 gnullvm, LLVM-MinGW20260908/LLVM23.1.1 and Docutils0.22.2 are installed
|
||||
under ignored task-local directories. Locked dependency fetch completed despite
|
||||
transient network timeouts. Full current core all-targets is running offline with
|
||||
one compiler job, log `.local/ytr83r-core-all-targets.log`, session41189. Completion
|
||||
must be read from `.local/ytr83r-core-all-targets-status.json`; a running compile
|
||||
is not a pass. Server/desktop gates follow sequentially. Packaging11/11 and
|
||||
synthetic Windows notices passed. [Host setup/evidence](release-host-bootstrap-ytr83r-20260911.md).
|
||||
|
||||
Local B4 rendering implementation owns `behavior.rs`/`scenario_lifecycle` only:
|
||||
true visible root/empty-operand lifetime bounds, complete lane rendering,
|
||||
selectable/aligned lifecycle markers and activation clipping to definite alive
|
||||
intervals. Independent code/spec review is complete; it closed self-caption overlap
|
||||
and two minimum-Fit hit-selection counterexamples. Ten real-Scene tests await compilation. The
|
||||
12-scenario fixed-ID native fixture/oracle is prepared, not yet compiled/admitted.
|
||||
Root updated language section8.1 and corrected section4.3's existing structural
|
||||
ordering contract; independent review resolved the loop-iteration wording.
|
||||
|
||||
Portable QEMU11.1.0 is verified and a normal five-minute TCG pilot is prepared,
|
||||
but the first Ubuntu image transfer timed out at11,714,560/264,372,224bytes. A bounded
|
||||
3-MiB range experiment verified matching HTTPS overlaps averaging507.37KiB/s;
|
||||
the worker is attempting a600-second HTTPS resume with a128KiB/s low-speed stop.
|
||||
No guest boot or Linux tests have yet occurred.
|
||||
[Exact resume/preflight commands](release-linux-host-ytr83r-20260911.md).
|
||||
|
||||
## Local ownership and next checks
|
||||
|
||||
Root owns Git integration, source acceptance, serialized Cargo builds, native UI
|
||||
@@ -38,7 +69,7 @@ their updates; do not duplicate their implementations.
|
||||
Next: finish the combined-source checkpoint, run appropriate current compilation,
|
||||
focused/full desktop and strict checks under one writer, then exercise actual
|
||||
Windows creation/scenario/history/recovery tasks with independent whole-model and
|
||||
source oracles. Obtain the corrected creation oracle before using its Undo test.
|
||||
source oracles. Use the imported corrected creation oracle with its explicit Undo origin.
|
||||
Continue disjoint semantic/lifecycle/platform work while external slices mature.
|
||||
|
||||
## Attributed prior-host evidence
|
||||
@@ -54,21 +85,23 @@ They do not establish current Codex-ytr83r execution.
|
||||
| Packaging | Imported b98 lineage retains25 notice bytes; prior-host synthetic package11/11 and Windows notice checks passed. Matching optimized distributions and Linux runtime acceptance remain open. |
|
||||
| Source authoring | Imported complete40-identity source-only fixture and26-request REST minimal examples establish bounded identity/source/refactoring/persistence facts. [Design/probe](release-textual-authoring-design-20260911.md), [REST usability experiment](release-textual-usability-audit-20260911.md). Large-model human/AI usability remains open. |
|
||||
| Semantic experiments | Baseline versus complete-case experiment passed218 assertions in its isolated program. Property-owned baselines plus one-level case overrides selected for contract refinement; this is not implemented Studio analysis/library semantics. [Experiment](release-analysis-context-experiment-20260911.md), [decision](DECISIONS.md). |
|
||||
| Later external creation native work | Wiki/issue #138/#140/#142 reports named Process/Scenario/Machine7/8/9-source creation, identity-preserving Undo/Redo/reopen and pending SA recovery19 checks. Exact later evidence and corrected v2 oracle are outside4bb at this checkpoint; obtain their coherent transfer. |
|
||||
| Later external creation native work | Evidence2ec imported at b853a1e records named Process/Scenario/Machine7/8/9-source creation, identity-preserving Undo/Redo/reopen and SA recovery checks on that host's binary, with exact corrected v2 oracle. These are attributed imported passes, not new local execution. |
|
||||
|
||||
## Known remaining gaps
|
||||
|
||||
- B4 lifecycle is cfg(test) projection only. Production integration needs real
|
||||
visible lifetime extents, branch-local activation clipping at Delete, complete
|
||||
lane geometry and self-message endpoint alignment. [Independent review](release-scenario-lifecycle-independent-review-20260911.md).
|
||||
- B4 lifecycle production integration is local and not yet accepted. Source now
|
||||
implements visible lifetime extents, branch-local activation clipping, complete
|
||||
lanes and aligned markers/self messages; runtime verification must establish these.
|
||||
[Implementation and independent findings](release-lifecycle-render-ytr83r-20260911.md).
|
||||
- Blank new Process/Scenario first action and missing prerequisite detours are
|
||||
addressed by the external staged-source owner; qualified Action allocation is
|
||||
a further explicit slice. Chooser production adapters/native acceptance remain open.
|
||||
- The transferred creation checker is v1 and has a known invalid disk-equals-reachable
|
||||
Undo assumption. Issue #132 v2 requires explicit pre-Undo origin and exact retained
|
||||
detached files. Do not weaken full accepted Project/reachable-source equality.
|
||||
- Source constructor-default documentation/probes after4bb, source CLI, and refined
|
||||
analysis/library contracts are not imported merely because their Wiki summaries exist.
|
||||
- The corrected creation checker v2 is now imported through2ec72d4. Its explicit
|
||||
pre-Undo origin and exact retained-detached-file contract still need execution on
|
||||
this host; do not weaken full accepted Project/reachable-source equality.
|
||||
- Source constructor-default documentation/probes after4bb are now imported;
|
||||
the source CLI and refined analysis/library contracts are not imported merely
|
||||
because their Wiki summaries exist.
|
||||
- Calculations, quantity/unit algebra and maintained libraries require coherent
|
||||
contracts and complete core/source/API/GUI/reference/deletion/transition/history
|
||||
realization. Current metadata and includes are insufficient.
|
||||
|
||||
+14
-5
@@ -17,12 +17,13 @@ remain future scope. Compilation or a pleasant happy path is not completion.
|
||||
## Verified facts
|
||||
|
||||
- Local start was cleanff615d0. Root decoded/hash-verified/imported published3978,
|
||||
c6 and4bb bundles; merged c6 as c81e285 and is integrating creation4bb. There were
|
||||
c6 and4bb bundles; merged c6 as c81e285 and integrated creation4bb at e207b04. There were
|
||||
no production cherry-pick conflicts; docs/attributes require evidence union.
|
||||
The current goal is copied from the user's latest file.
|
||||
- Read-only integration comparison finds equivalent workspace/Sensor code in c6;
|
||||
replaying3978 or d114 is unnecessary. Core source is unchanged betweenff andc6.
|
||||
B4 is still a test-only projection. [Audit](release-integration-ytr83r-20260911.md).
|
||||
B4 now has a production Scene bridge and independently reviewed source, with
|
||||
compilation/native acceptance pending. [Audit](release-integration-ytr83r-20260911.md).
|
||||
- Imported source implements named behavior creation, exact prepared-batch
|
||||
correlation, success-only selection, raw-draft guards, recoverable original
|
||||
layer and modal-priority Escape. These are source facts; combined local runtime
|
||||
@@ -32,7 +33,15 @@ remain future scope. Compilation or a pleasant happy path is not completion.
|
||||
[STATE.md](STATE.md). They are not new local passes.
|
||||
- The original transferred creation oracle incorrectly equates physical disk files
|
||||
with reachable sources during Undo. Later external v2 corrects the evaluator
|
||||
with explicit origin/retained-byte rules; exact v2 is not yet in this transfer.
|
||||
with explicit origin/retained-byte rules; exact v2/evidence2ec was imported at
|
||||
b853a1e after complete path/hash preflight. New host execution is still pending.
|
||||
- Isolated Rust1.98.1/LLVM23.1.1 and locked dependencies are available. Packaging
|
||||
11/11 and synthetic Windows notice checks pass; full core all-targets currently
|
||||
compiles under one Cargo job. Strict reST/HTML generation passes with Docutils0.22.2.
|
||||
- B4 independent criticism found and resolved self-caption overlap and two dense
|
||||
minimum-Fit hit-selection counterexamples. Shared nearest marker/message-stroke
|
||||
arbitration preserves exact marker/header/label selection. Ten real-Scene tests
|
||||
and a separate fixed-ID twelve-scenario native oracle await execution.
|
||||
- The separate baseline-context experiment passed218 assertions; explicit
|
||||
one-level overrides reduce repeated stable assumptions while complete frozen
|
||||
definition environments remain necessary. Proposed syntax is not source7.
|
||||
@@ -102,7 +111,7 @@ has its own report. Codex-9e8r6t owns staged sources/first actions and source CL
|
||||
Codex-956atf owns chooser and semantic contracts. Read Gitea issue #1/Wiki before
|
||||
resuming, obtain exact coherent transfer heads, and respect their source ownership.
|
||||
|
||||
First finish and verify the integrated current source. Obtain corrected evaluator
|
||||
and later evidence. Then close real remaining lifetime, modeling/textual,
|
||||
First finish and verify the integrated current source using the imported corrected
|
||||
evaluator and newly prepared independent lifetime oracle. Then close remaining modeling/textual,
|
||||
analysis/library and platform gates with independent criticism and whole-task
|
||||
acceptance. Update facts, assumptions and decisions as evidence changes them.
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Independent native lifecycle fixture — 2026-09-11
|
||||
|
||||
Owner: Codex-ytr83r /root/integration_audit. **Prepared, uncompiled and unexecuted.**
|
||||
Root must first compile the current core and checker, run ordinary source admission
|
||||
and self-check, and generate the immutable baseline before opening Studio. No native
|
||||
or Scene acceptance is claimed here. The renderer implementation is independently
|
||||
owned by /root/server_audit; this oracle does not import any desktop module.
|
||||
|
||||
## Build and source admission
|
||||
|
||||
Compile `harness/lifecycle-native-contract.rs` with the matching `oa_core` rlib and
|
||||
dependency search directory, Rust2024 and warnings denied. No separate serde_json
|
||||
dependency is needed. Root owns the writer/toolchain lease. Example shape:
|
||||
|
||||
```text
|
||||
rustc --edition=2024 -D warnings harness/lifecycle-native-contract.rs --extern oa_core=ABSOLUTE_MATCHING_RLIB -L dependency=ABSOLUTE_DEPS -o ABSOLUTE_ISOLATED_EXE
|
||||
lifecycle-native-contract.exe self-check
|
||||
lifecycle-native-contract.exe inventory
|
||||
lifecycle-native-contract.exe generate baseline FRESH_BASE_DIR
|
||||
lifecycle-native-contract.exe check baseline FRESH_BASE_DIR/project.arcadia
|
||||
lifecycle-native-contract.exe generate oa-rename FRESH_EXPECTED_OA_DIR
|
||||
lifecycle-native-contract.exe generate sa-rename FRESH_EXPECTED_SA_DIR
|
||||
```
|
||||
|
||||
Keep logs, screenshot files, hashes and model exports **outside** each checkpoint
|
||||
directory. A checkpoint contains only its complete saved project files; the checker
|
||||
compares every physical file, not just files with `.arcadia` extension. Generated
|
||||
base is two sources: canonical project plus an intentionally CRLF Unicode note.
|
||||
The two expected rename directories must be generated before native output is read.
|
||||
All element/relation IDs are fixed; SA port UUIDv5 values derive from the fixed
|
||||
functional-exchange ID through the authoritative port planner. No observed identity,
|
||||
geometry, order, reference or name is borrowed into predictions.
|
||||
|
||||
The unchecked fixture builder is only preparation: final `execute_batch`, source
|
||||
compile and load/recompile equality are mandatory. Self-Delete and unassigned
|
||||
lifeline cases intentionally use explicit local sender/receiver with `exchange=None`.
|
||||
If current core refuses either, record the exact refusal and revise or separate that
|
||||
case under root review; do not bypass admission or silently remove validation.
|
||||
Self-Create is deliberately absent because its admission was not established.
|
||||
Expected unfinished-model warnings (unbound exchange/representation, unanswered
|
||||
Call) are diagnostic evidence, not permission to bypass errors. Capture the actual
|
||||
warning set after source admission.
|
||||
|
||||
## Fixed identities and navigation
|
||||
|
||||
All numeric offsets below are **decimal** added to UUID integer
|
||||
`5ceab400-0000-4000-8000-000000000000`. Project is offset1. Add side base1000 for OA
|
||||
or2000 for SA, then the listed local offset. Names begin OA/SA for explorer search.
|
||||
Each scenario has at most two lanes and explicit OperationalActivity/Functional
|
||||
viewpoint; no hidden legacy scenario mode is used.
|
||||
|
||||
| Scenario local offset/name | Lanes | Occurrences and declared order |
|
||||
| --- | --- | --- |
|
||||
|100 `01 Root create action delete`|101 controller,102 receiver|110 Create receiver →111 Receiver action →112 Delete receiver|
|
||||
|200 `02 Preexisting empty`|201 controller,202 receiver|Empty root|
|
||||
|300 `03 Create only`|301 controller,302 receiver|310 Create only receiver|
|
||||
|400 `04 Conditional delete`|401 controller,402 receiver|410 Open call →411 ALT;412 left operand contains414 Conditional delete;413 right operand empty|
|
||||
|500 `05 Unassigned receiver`|501 controller,502 represented=None|510 Create unassigned receiver →511 Delete unassigned receiver; both exchanges unresolved|
|
||||
|600 `06 Self delete`|601 controller only|610 Self delete controller; sender=receiver601, exchange unresolved|
|
||||
|
||||
Controller/receiver work are local10/11; ordinary service relation is12. Root
|
||||
Delete rename target is side+112. The exact new name is `OA Delete receiver · reviewed`
|
||||
or `SA Delete receiver · reviewed`. Aliases, IDs and every other field stay unchanged.
|
||||
For direct selection, OA root scenario is `5ceab400-0000-4000-8000-00000000044c`
|
||||
and OA Delete is `5ceab400-0000-4000-8000-000000000458`; SA root scenario is
|
||||
`5ceab400-0000-4000-8000-000000000834` and SA Delete is
|
||||
`5ceab400-0000-4000-8000-000000000840`. `inventory` emits the complete admitted
|
||||
scenario/lifeline/occurrence ID/name/parent table and actual validation diagnostics.
|
||||
|
||||
## Native procedure and independent observations
|
||||
|
||||
Use a copied working directory and isolated profile with an immutable matching GUI.
|
||||
Record source commit, executable SHA256, actual client dimensions, input sequence,
|
||||
screenshots and saved checkpoints outside the project directories. Open the actual
|
||||
`project.arcadia` through Studio. Select Behavior & scenarios, the matching layer,
|
||||
and the exact prefixed scenario. First run the OA matrix; repeat SA independently
|
||||
from the same baseline. Neither a passing source checker nor pure Scene tests imply
|
||||
the following visual/interaction results.
|
||||
|
||||
| Step | Native observation/evidence required | Semantic oracle |
|
||||
| --- | --- | --- |
|
||||
|Root lifecycle|Controller remains usable; receiver lifetime begins at Create and ends at Delete; Action lies between. Inspect birth/death hit targets and corresponding Properties identity. Clicking an intended lifecycle target must not select a different occurrence.|Save copied baseline; `check baseline`|
|
||||
|Preexisting empty|Both lane headers/guides visible through the actual empty canvas; no invented birth/death. Neutral insertion guide remains usable even when no occurrence exists.|Save/navigation must remain `baseline`|
|
||||
|Create-only|Receiver lifetime continues beyond the last occurrence to visible root extent; do not confuse insertion endpoint with lifetime endpoint.|`check baseline`|
|
||||
|Conditional Delete|Left receiver activation/lifetime ends at its Delete. Right empty branch retains independent receiver state; Delete must not terminate sibling activation. Inspect unresolved-call indication without inventing Reply or silently rewriting the model.|`check baseline`|
|
||||
|Unassigned receiver|Incomplete lane remains visible, with explicit receiver lifecycle targets/hit geometry. It must not disappear or crash because placement eligibility excludes unresolved representation. Record whatever completeness diagnostic is actually shown.|`check baseline`; only after admitted|
|
||||
|Self Delete|Single lane's self-message end and death marker align and remain selectable. Inspect Properties and hit targets at ordinary100% then Fit; do not infer self-Create behavior.|`check baseline`; only after admitted|
|
||||
|Narrow/overview|At actual1100×700 and a wide client, exercise Fit,100%, scroll, selection and Tools. Guides/markers remain correctly aligned and hit-testable; record font wrapping/overlap and clipped regions.|Save unchanged complete baseline|
|
||||
|Real rename|Select root Delete occurrence by actual marker/message, verify identity, edit Name to exact side-specific reviewed name and Apply, or make precisely that source name edit and Apply. Save once; preserve complete checkpoint before Undo.|`check oa-rename` or `check sa-rename`; exact previously generated expectation via `same`|
|
||||
|Undo|One Undo restores old name, exact IDs and complete source. Save and freeze checkpoint.|`check baseline UNDO_ENTRY RENAMED_ENTRY`, then `same BASE_ENTRY UNDO_ENTRY RENAMED_ENTRY`|
|
||||
|Redo/reopen|One Redo and Save restores exact first rename checkpoint. Reopen through actual file picker and Save again. Observe lifecycle geometry/selection afresh.|`check SIDE-rename`, `same FIRST_RENAME REDO`, `same FIRST_RENAME REOPEN`|
|
||||
|
||||
Each rename mode starts from baseline and changes only its own layer. Undo OA before
|
||||
starting SA or use a fresh baseline copy; `sa-rename` does not include an earlier OA
|
||||
rename. Direct additional scenario insertion is outside these fixed rename modes:
|
||||
define a separate prediction before using it as acceptance evidence.
|
||||
|
||||
## Exact physical-file and Undo policy
|
||||
|
||||
`check MODE ENTRY` independently constructs complete expected Project and reachable
|
||||
SourceBundle, then requires the physical file map to equal those expected sources.
|
||||
`check MODE ENTRY PRE_UNDO_ENTRY` additionally permits only the exact original files
|
||||
present in the explicitly supplied pre-Undo checkpoint. Expected reachable bytes
|
||||
overlay those origin files; missing, unknown or changed retained files fail. No
|
||||
automatic disk-equals-reachable assertion is imposed on a valid Undo with retained
|
||||
files. The explicit origin must be a separately frozen, previously verified accepted
|
||||
checkpoint; do not supply a directory manufactured after seeing an unexpected file.
|
||||
|
||||
`same EXPECTED ACTUAL` pins all identities/model/source and the complete saved physical
|
||||
map of a previously verified checkpoint. Its optional pre-Undo origin uses the same
|
||||
exact retention rule. It never accepts source drift just because decoded models agree.
|
||||
These rename cases normally create no detached files, but the checker explicitly
|
||||
self-tests missing-origin, unknown/missing/changed retained-file discrimination.
|
||||
|
||||
## Status
|
||||
|
||||
Source review/preparation only. Pending: direct-rustc compile, self-check, core/source
|
||||
admission and actual warning inventory, generated baseline and pre-native hashes,
|
||||
independent adversarial review, matching GUI and all native steps above. Root records
|
||||
execution results without transferring prior-host acceptance to this new fixture.
|
||||
@@ -0,0 +1,570 @@
|
||||
//! Independent B4 native source fixture/oracle. No desktop renderer is linked.
|
||||
//! generate MODE DIR | check MODE ENTRY [PRE_UNDO_ENTRY] | same EXPECTED ACTUAL [PRE_UNDO_ENTRY]
|
||||
//! self-check | inventory; modes: baseline, oa-rename, sa-rename. All fixture identities are fixed.
|
||||
use oa_core::{source_project::*, *};
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
type Disk = BTreeMap<String, Vec<u8>>;
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(0x5ceab400000040008000000000000000 + n)
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct Side {
|
||||
layer: Layer,
|
||||
word: &'static str,
|
||||
base: u128,
|
||||
}
|
||||
const SIDES: [Side; 2] = [
|
||||
Side {
|
||||
layer: Layer::Oa,
|
||||
word: "OA",
|
||||
base: 1000,
|
||||
},
|
||||
Side {
|
||||
layer: Layer::Sa,
|
||||
word: "SA",
|
||||
base: 2000,
|
||||
},
|
||||
];
|
||||
impl Side {
|
||||
fn id(self, n: u128) -> Id {
|
||||
id(self.base + n)
|
||||
}
|
||||
fn name(self, text: &str) -> String {
|
||||
format!("{} {text}", self.word)
|
||||
}
|
||||
}
|
||||
fn element(p: &mut Project, side: Side, n: u128, kind: Kind, name: &str, parent: Option<u128>) {
|
||||
let mut e = p
|
||||
.make_element(
|
||||
kind,
|
||||
side.layer,
|
||||
&side.name(name),
|
||||
parent.map(|n| side.id(n)),
|
||||
)
|
||||
.unwrap();
|
||||
e.id = side.id(n);
|
||||
e.alias = format!("life_{}_{}", side.word.to_lowercase(), n);
|
||||
p.elements.insert(e.id, e);
|
||||
}
|
||||
fn scenario(p: &mut Project, side: Side, n: u128, name: &str) {
|
||||
element(p, side, n, Kind::Scenario, name, None);
|
||||
let Some(SharedBody::Scenario { viewpoint, .. }) =
|
||||
p.elements.get_mut(&side.id(n)).unwrap().shared_mut()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
*viewpoint = if side.layer == Layer::Oa {
|
||||
ScenarioViewpoint::OperationalActivity
|
||||
} else {
|
||||
ScenarioViewpoint::Functional
|
||||
};
|
||||
}
|
||||
fn line(p: &mut Project, side: Side, n: u128, owner: u128, work: Option<u128>, name: &str) {
|
||||
element(p, side, n, Kind::Lifeline, name, Some(owner));
|
||||
*p.elements
|
||||
.get_mut(&side.id(n))
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Lifeline {
|
||||
represented: work.map(|n| side.id(n)),
|
||||
};
|
||||
}
|
||||
fn occurrence(
|
||||
p: &mut Project,
|
||||
side: Side,
|
||||
n: u128,
|
||||
parent: u128,
|
||||
name: &str,
|
||||
body: OccurrenceBody,
|
||||
) {
|
||||
element(p, side, n, Kind::Occurrence, name, Some(parent));
|
||||
*p.elements
|
||||
.get_mut(&side.id(n))
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
}
|
||||
fn steps(p: &mut Project, side: Side, owner: u128, ordered: &[u128]) {
|
||||
*p.elements
|
||||
.get_mut(&side.id(owner))
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap() = ordered.iter().map(|n| side.id(*n)).collect();
|
||||
}
|
||||
fn message(
|
||||
side: Side,
|
||||
sender: u128,
|
||||
receiver: u128,
|
||||
kind: MessageKind,
|
||||
resolved: bool,
|
||||
) -> OccurrenceBody {
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: resolved.then(|| side.id(12)),
|
||||
sender: Some(side.id(sender)),
|
||||
receiver: Some(side.id(receiver)),
|
||||
message_kind: kind,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
}
|
||||
}
|
||||
fn fixture() -> Project {
|
||||
let mut p = Project::empty();
|
||||
p.id = id(1);
|
||||
p.alias = "lifecycle_native".into();
|
||||
p.name = "Lifecycle native contract".into();
|
||||
p.description =
|
||||
"Fixed semantic inputs; lifecycle geometry is derived and never authored.".into();
|
||||
for side in SIDES {
|
||||
let work = if side.layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
element(&mut p, side, 10, work, "Controller work", None);
|
||||
element(&mut p, side, 11, work, "Receiver work", None);
|
||||
if side.layer == Layer::Oa {
|
||||
let relation = Relation {
|
||||
id: side.id(12),
|
||||
alias: "life_oa_service".into(),
|
||||
name: "OA Lifecycle service".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::new(RelationKind::Interaction, side.id(10), side.id(11)),
|
||||
};
|
||||
p.relations.insert(relation.id, relation);
|
||||
} else {
|
||||
// The port planner derives deterministic UUIDv5 endpoints from this fixed exchange ID.
|
||||
let commands = ports::plan_functional_exchange(
|
||||
&p,
|
||||
&ports::FunctionalExchangeRequest {
|
||||
id: side.id(12),
|
||||
alias: "life_sa_service".into(),
|
||||
name: "SA Lifecycle service".into(),
|
||||
description: String::new(),
|
||||
source: side.id(10),
|
||||
target: side.id(11),
|
||||
source_port: ports::PortChoice::Create,
|
||||
target_port: ports::PortChoice::Create,
|
||||
items: vec![],
|
||||
},
|
||||
)
|
||||
.expect("typed fixed-ID functional exchange must be admitted");
|
||||
p = execute_batch(&p, &commands).unwrap();
|
||||
}
|
||||
for (n, name) in [
|
||||
(100, "01 Root create action delete"),
|
||||
(200, "02 Preexisting empty"),
|
||||
(300, "03 Create only"),
|
||||
(400, "04 Conditional delete"),
|
||||
(500, "05 Unassigned receiver"),
|
||||
(600, "06 Self delete"),
|
||||
] {
|
||||
scenario(&mut p, side, n, name);
|
||||
line(&mut p, side, n + 1, n, Some(10), "Controller lane");
|
||||
if n != 600 {
|
||||
line(
|
||||
&mut p,
|
||||
side,
|
||||
n + 2,
|
||||
n,
|
||||
(n != 500).then_some(11),
|
||||
if n == 500 {
|
||||
"Unassigned receiver lane"
|
||||
} else {
|
||||
"Receiver lane"
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
110,
|
||||
100,
|
||||
"Create receiver",
|
||||
message(side, 101, 102, MessageKind::Create, true),
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
111,
|
||||
100,
|
||||
"Receiver action",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(side.id(11)),
|
||||
lifeline: Some(side.id(102)),
|
||||
},
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
112,
|
||||
100,
|
||||
"Delete receiver",
|
||||
message(side, 101, 102, MessageKind::Delete, true),
|
||||
);
|
||||
steps(&mut p, side, 100, &[110, 111, 112]);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
310,
|
||||
300,
|
||||
"Create only receiver",
|
||||
message(side, 301, 302, MessageKind::Create, true),
|
||||
);
|
||||
steps(&mut p, side, 300, &[310]);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
410,
|
||||
400,
|
||||
"Open call",
|
||||
message(side, 401, 402, MessageKind::Call, true),
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
411,
|
||||
400,
|
||||
"ALT delete or keep",
|
||||
OccurrenceBody::Fragment {
|
||||
operator: FragmentOperator::Alternative,
|
||||
branches: vec![side.id(412), side.id(413)],
|
||||
},
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
412,
|
||||
411,
|
||||
"Left delete operand",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "stop_requested".into(),
|
||||
occurrences: vec![side.id(414)],
|
||||
},
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
413,
|
||||
411,
|
||||
"Right empty operand",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "else".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
414,
|
||||
412,
|
||||
"Conditional delete receiver",
|
||||
message(side, 401, 402, MessageKind::Delete, true),
|
||||
);
|
||||
steps(&mut p, side, 400, &[410, 411]);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
510,
|
||||
500,
|
||||
"Create unassigned receiver",
|
||||
message(side, 501, 502, MessageKind::Create, false),
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
511,
|
||||
500,
|
||||
"Delete unassigned receiver",
|
||||
message(side, 501, 502, MessageKind::Delete, false),
|
||||
);
|
||||
steps(&mut p, side, 500, &[510, 511]);
|
||||
occurrence(
|
||||
&mut p,
|
||||
side,
|
||||
610,
|
||||
600,
|
||||
"Self delete controller",
|
||||
message(side, 601, 601, MessageKind::Delete, false),
|
||||
);
|
||||
steps(&mut p, side, 600, &[610]);
|
||||
}
|
||||
execute_batch(&p, &[]).expect("complete source fixture must satisfy ordinary core admission; never bypass self/incomplete-lane rejection")
|
||||
}
|
||||
fn baseline() -> CompiledSourceProject {
|
||||
let p = fixture();
|
||||
let entry = language::format(&p).replacen("{\n", "{\n include \"rationale.arcadia\";\n", 1);
|
||||
let raw = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
|
||||
("project.arcadia".into(), entry),
|
||||
("rationale.arcadia".into(), "// Preserve lifecycle note: 雨 · receiver.\r\n// Geometry never changes these bytes.\r\n".into()),
|
||||
]) };
|
||||
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, p);
|
||||
assert_eq!(
|
||||
compiled.bundle, raw,
|
||||
"no identity insertion, normalization or migration"
|
||||
);
|
||||
compiled
|
||||
}
|
||||
fn expected(mode: &str) -> CompiledSourceProject {
|
||||
let base = baseline();
|
||||
let side = match mode {
|
||||
"baseline" => return base,
|
||||
"oa-rename" => SIDES[0],
|
||||
"sa-rename" => SIDES[1],
|
||||
_ => panic!("mode: baseline|oa-rename|sa-rename"),
|
||||
};
|
||||
let next = execute_batch(
|
||||
&base.decoded.project,
|
||||
&[Command::Rename(
|
||||
side.id(112),
|
||||
side.name("Delete receiver · reviewed"),
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
update_bundle(&base, &next, ProjectLimits::default()).unwrap()
|
||||
}
|
||||
fn disk(root: &Path, here: &Path, out: &mut Disk) {
|
||||
for entry in fs::read_dir(here).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let kind = entry.file_type().unwrap();
|
||||
assert!(!kind.is_symlink(), "checkpoint cannot contain symlinks");
|
||||
if kind.is_dir() {
|
||||
disk(root, &entry.path(), out);
|
||||
} else {
|
||||
assert!(kind.is_file(), "checkpoint files must be ordinary files");
|
||||
let path = entry.path();
|
||||
let name = path
|
||||
.strip_prefix(root)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('\\', "/");
|
||||
out.insert(name, fs::read(path).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Snapshot {
|
||||
compiled: CompiledSourceProject,
|
||||
disk: Disk,
|
||||
}
|
||||
fn load(entry: &Path) -> Snapshot {
|
||||
assert_eq!(entry.file_name().unwrap(), "project.arcadia");
|
||||
let raw = source_files::load_project(entry, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.reachable_bundle();
|
||||
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle, raw, "no implicit source normalization");
|
||||
execute_batch(&compiled.decoded.project, &[]).unwrap();
|
||||
let mut files = Disk::new();
|
||||
let root = entry.parent().unwrap();
|
||||
disk(root, root, &mut files);
|
||||
Snapshot {
|
||||
compiled,
|
||||
disk: files,
|
||||
}
|
||||
}
|
||||
fn source_disk(bundle: &SourceBundle) -> Disk {
|
||||
bundle
|
||||
.files
|
||||
.iter()
|
||||
.map(|(name, text)| (name.clone(), text.as_bytes().to_vec()))
|
||||
.collect()
|
||||
}
|
||||
fn expected_disk(bundle: &SourceBundle, origin: Option<&Disk>) -> Disk {
|
||||
let mut files = origin.cloned().unwrap_or_default();
|
||||
files.extend(source_disk(bundle));
|
||||
files
|
||||
}
|
||||
fn equal(expected: &CompiledSourceProject, actual: &Snapshot, origin: Option<&Disk>) {
|
||||
assert_eq!(
|
||||
actual.compiled.decoded.project, expected.decoded.project,
|
||||
"whole Project: every identity, field, order, reference and geometry"
|
||||
);
|
||||
assert_eq!(
|
||||
actual.compiled.bundle, expected.bundle,
|
||||
"whole reachable SourceBundle including retained note bytes"
|
||||
);
|
||||
assert_eq!(
|
||||
actual.disk,
|
||||
expected_disk(&expected.bundle, origin),
|
||||
"whole physical file map: only exact pre-Undo retained files are permitted"
|
||||
);
|
||||
}
|
||||
fn self_check() {
|
||||
let base = baseline();
|
||||
assert_eq!(
|
||||
base.decoded.project,
|
||||
fixture(),
|
||||
"fixture deterministic, including typed ports"
|
||||
);
|
||||
assert_eq!(base.bundle, baseline().bundle);
|
||||
for mode in ["baseline", "oa-rename", "sa-rename"] {
|
||||
let prediction = expected(mode);
|
||||
let actual = Snapshot {
|
||||
disk: source_disk(&prediction.bundle),
|
||||
compiled: expected(mode),
|
||||
};
|
||||
equal(&prediction, &actual, None);
|
||||
if mode != "baseline" {
|
||||
assert_ne!(prediction.decoded.project, base.decoded.project);
|
||||
assert_ne!(prediction.bundle, base.bundle);
|
||||
let side = if mode == "oa-rename" {
|
||||
SIDES[0]
|
||||
} else {
|
||||
SIDES[1]
|
||||
};
|
||||
let mut independently_named = base.decoded.project.clone();
|
||||
independently_named
|
||||
.elements
|
||||
.get_mut(&side.id(112))
|
||||
.unwrap()
|
||||
.name = side.name("Delete receiver · reviewed");
|
||||
assert_eq!(
|
||||
prediction.decoded.project, independently_named,
|
||||
"rename changes precisely one name; alias and identity stay fixed"
|
||||
);
|
||||
}
|
||||
let mut changed_project = prediction.decoded.project.clone();
|
||||
changed_project.description.push_str("unrequested");
|
||||
assert_ne!(changed_project, prediction.decoded.project);
|
||||
let mut changed_source = prediction.bundle.clone();
|
||||
changed_source
|
||||
.files
|
||||
.get_mut("rationale.arcadia")
|
||||
.unwrap()
|
||||
.push_str("// unrequested\n");
|
||||
assert_ne!(changed_source, prediction.bundle);
|
||||
}
|
||||
let plain = source_disk(&base.bundle);
|
||||
let mut origin = plain.clone();
|
||||
origin.insert(
|
||||
"detached.arcadia".into(),
|
||||
b"// exact pre-Undo bytes\r\n".to_vec(),
|
||||
);
|
||||
let retained = expected_disk(&base.bundle, Some(&origin));
|
||||
assert_eq!(retained, origin);
|
||||
assert_ne!(
|
||||
retained,
|
||||
expected_disk(&base.bundle, None),
|
||||
"origin is mandatory to authorize retained files"
|
||||
);
|
||||
let mut missing = retained.clone();
|
||||
missing.remove("detached.arcadia");
|
||||
assert_ne!(missing, retained);
|
||||
let mut changed = retained.clone();
|
||||
changed.get_mut("detached.arcadia").unwrap().push(b'!');
|
||||
assert_ne!(changed, retained);
|
||||
let mut unknown = retained.clone();
|
||||
unknown.insert("unknown.arcadia".into(), vec![]);
|
||||
assert_ne!(unknown, retained);
|
||||
let mut wrong_reachable = origin.clone();
|
||||
wrong_reachable.insert("rationale.arcadia".into(), b"wrong prior contents".to_vec());
|
||||
assert_eq!(
|
||||
expected_disk(&base.bundle, Some(&wrong_reachable)),
|
||||
retained,
|
||||
"expected reachable bytes override the origin"
|
||||
);
|
||||
println!(
|
||||
"PASS self-check: fixed OA/SA fixture, exact name-only predictions, explicit retained-file origin policy"
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
assert_eq!(SCHEMA_VERSION, 7);
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
match args.get(1).map(String::as_str) {
|
||||
Some("self-check") => {
|
||||
assert_eq!(args.len(), 2);
|
||||
self_check();
|
||||
}
|
||||
Some("inventory") => {
|
||||
assert_eq!(args.len(), 2);
|
||||
let base = baseline();
|
||||
for e in
|
||||
base.decoded.project.elements.values().filter(|e| {
|
||||
matches!(e.kind(), Kind::Scenario | Kind::Lifeline | Kind::Occurrence)
|
||||
})
|
||||
{
|
||||
println!(
|
||||
"{} | {:?} | {} | parent={:?}",
|
||||
e.id,
|
||||
e.kind(),
|
||||
e.name,
|
||||
e.parent
|
||||
);
|
||||
}
|
||||
for issue in validation::validate(&base.decoded.project) {
|
||||
println!("DIAGNOSTIC {issue:?}");
|
||||
}
|
||||
}
|
||||
Some("generate") => {
|
||||
assert_eq!(args.len(), 4, "generate MODE FRESH_DIR");
|
||||
let prediction = expected(&args[2]);
|
||||
let dir = Path::new(&args[3]);
|
||||
assert!(!dir.exists(), "preserve existing fixture/evidence");
|
||||
fs::create_dir_all(dir).unwrap();
|
||||
for (name, text) in &prediction.bundle.files {
|
||||
let path = dir.join(name);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, text.as_bytes()).unwrap();
|
||||
}
|
||||
equal(&prediction, &load(&dir.join("project.arcadia")), None);
|
||||
println!(
|
||||
"PASS generated {}: {} fixed elements, {} relations, {} exact sources",
|
||||
args[2],
|
||||
prediction.decoded.project.elements.len(),
|
||||
prediction.decoded.project.relations.len(),
|
||||
prediction.bundle.files.len()
|
||||
);
|
||||
}
|
||||
Some("check") => {
|
||||
assert!(
|
||||
(4..=5).contains(&args.len()),
|
||||
"check MODE ENTRY [PRE_UNDO_ENTRY]"
|
||||
);
|
||||
let actual = load(Path::new(&args[3]));
|
||||
let origin = args.get(4).map(|path| load(Path::new(path)));
|
||||
equal(
|
||||
&expected(&args[2]),
|
||||
&actual,
|
||||
origin.as_ref().map(|s| &s.disk),
|
||||
);
|
||||
println!(
|
||||
"PASS {}: complete Project/reachable sources/physical map",
|
||||
args[2]
|
||||
);
|
||||
}
|
||||
Some("same") => {
|
||||
assert!(
|
||||
(4..=5).contains(&args.len()),
|
||||
"same VERIFIED_EXPECTED_ENTRY ACTUAL_ENTRY [PRE_UNDO_ENTRY]"
|
||||
);
|
||||
let wanted = load(Path::new(&args[2]));
|
||||
let actual = load(Path::new(&args[3]));
|
||||
let origin = args.get(4).map(|path| load(Path::new(path)));
|
||||
assert_eq!(
|
||||
actual.compiled.decoded.project,
|
||||
wanted.compiled.decoded.project
|
||||
);
|
||||
assert_eq!(actual.compiled.bundle, wanted.compiled.bundle);
|
||||
let files = origin.as_ref().map_or_else(
|
||||
|| wanted.disk.clone(),
|
||||
|s| expected_disk(&wanted.compiled.bundle, Some(&s.disk)),
|
||||
);
|
||||
assert_eq!(
|
||||
actual.disk, files,
|
||||
"exact prior checkpoint or explicit retained-file origin"
|
||||
);
|
||||
println!("PASS same: all identities, fields, reachable source and physical bytes");
|
||||
}
|
||||
_ => panic!(
|
||||
"self-check | inventory | generate MODE DIR | check MODE ENTRY [PRE_UNDO_ENTRY] | same EXPECTED ACTUAL [PRE_UNDO_ENTRY]"
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# New Windows host build and integration record
|
||||
|
||||
Codex-ytr83r /root, 2026-09-11. This host started at clean `ff615d0` without
|
||||
Rust, a native C compiler or a WSL distribution. Its runtime evidence is separate
|
||||
from the imported prior-host reports. Full release acceptance remains open.
|
||||
|
||||
## Source and evidence integration
|
||||
|
||||
Behavior `c6d296a` is merged at `c81e285`; named creation `4bb4aaf` is integrated
|
||||
at `e207b04`. The current user attachment was copied exactly to PROJECT_GOAL.md.
|
||||
The independent integration review found no lost source semantics and retained
|
||||
both hosts' exact historical ledgers/reports. See the integration report and its
|
||||
13-blob archive manifest.
|
||||
|
||||
Additional evidence bundle `2ec72d4`, prerequisite `4bb4aaf`, is 185,563 bytes,
|
||||
SHA256 `F74E14EE394BCC026E24987C8526AA7E769DDE748FE38117532824EC19AE159F`.
|
||||
Hash and `git bundle verify` passed before import. A complete preflight compared
|
||||
all candidate files before writing: 132 new files, 58 already exact, and three
|
||||
reviewed replacements (creation checker v2 and its two instructions/reports).
|
||||
Local ledgers were preserved. Constructor-default specification changes were
|
||||
applied independently, preserving the new lifetime section. The exact import
|
||||
inventory is `.local/transfers/imported-evidence.json`. These are imported
|
||||
evidence artifacts, not newly executed native tests on this host.
|
||||
|
||||
## Isolated tools
|
||||
|
||||
Tools live under ignored `.local/toolchain`; the machine PATH, global Git identity
|
||||
and system Rust installation were not changed. Source `.local/toolchain/env.ps1`
|
||||
for task-local Cargo/Rustup paths, LLVM tools and one Cargo build job. Incremental
|
||||
compilation is disabled to limit disk use on this 8 GiB/two-CPU VM.
|
||||
|
||||
- Rust/Cargo: stable gnullvm 1.98.1; rustc `48a229cea 2026-09-01`,
|
||||
Cargo `797e8a9bc 2026-08-05`. rustup installation exited successfully.
|
||||
- LLVM-MinGW: official `llvm-mingw-20260908-ucrt-x86_64.zip`, 190,677,197 bytes;
|
||||
its SHA256 matches the release asset digest
|
||||
`1bcf74d06b724aeecaa6412ca85f5b26fb1da770e7cdcefa9263c9c5c3ad34b6`.
|
||||
Portable extraction succeeded. Clang and llvm-windres report LLVM 23.1.1.
|
||||
- Docutils 0.22.2 is installed under ignored `.local/python-deps`, matching
|
||||
the existing generated-document version. Bundled Python runs document and
|
||||
packaging tools; these are build/verification helpers, not product services.
|
||||
|
||||
References: [official Rust installation](https://rust-lang.org/tools/install/),
|
||||
[LLVM-MinGW releases](https://github.com/mstorsjo/llvm-mingw/releases/tag/20260908).
|
||||
Downloads showed intermittent stalls but completed without system network changes.
|
||||
|
||||
## Executed checks and limits
|
||||
|
||||
`python scripts/test-package-artifacts.py -v` passed all 11 tests in 1.808 s.
|
||||
`scripts/test-windows-notices.ps1` passed all synthetic notice/source/runtime
|
||||
preflight checks. The preserved temporary fixture is
|
||||
`C:/Temp/arcadia-notices-f75e1f32c33148749034b5fe8e0268aa`.
|
||||
These checks establish packaging invariants, not an actual packaged release.
|
||||
|
||||
The exact current core/server/desktop builds, renderer tests, native workflows,
|
||||
Linux runtime and optimized distributions are still pending. Cargo gates write
|
||||
logs and elapsed-time/exit JSON through `.local/run-gate.ps1`. Their later results
|
||||
must identify the tested source and must not inherit another host's test totals.
|
||||
@@ -0,0 +1,52 @@
|
||||
# B4 lifecycle Scene rendering — 2026-09-11
|
||||
|
||||
Author: Codex-ytr83r /root/server_audit, reassigned from the completed REST review to B4 rendering under root's Gitea issue #1 comment #155. Ownership announced in #157. Scope: behavior.rs lifecycle bridge, pure module declaration, dedicated rendering and Scene tests. No core semantics, schema/source/REST, staged-source creation, chooser or CLI changes; no Cargo or Git mutations. Root integrates, builds and controls native QA. Codex-ytr83r /root/integration_audit independently reviews the frozen source and owns the separate native fixture/oracle.
|
||||
|
||||
## Implementation and decisions
|
||||
|
||||
The frozen pure `scenario_lifecycle` traversal is now used in production. Its conservative local-tree rules remain unchanged: exact receiver IDs; no inferred receiver from an exchange; independent ALT/Optional/Loop/Parallel operands; uncertain continuation after a lifecycle-bearing fragment; opaque ScenarioUse; no Reply invention, model commands or serialization writes.
|
||||
|
||||
`behavior/scenario_lifecycle/render.rs` contains a separate Rendering record. Neutral dotted guides and stronger scoped Alive spines are distinct from message Lines. The complete rendering lane map includes unassigned lifelines; existing `Scene.lanes` remains the represented-lane authoring eligibility map. Neutral top headers say `Lane · name`. A small footer explains guide, lifetime and `?` uncertainty marks; unanswered call cues keep their existing ellipsis, avoiding two meanings for the same glyph.
|
||||
|
||||
The bridge gives root lifetime the actual visible span **85..scene.height−20**, leaving insertion/slot bounds unchanged. Empty operands also need a nonzero visible lifetime allowance: their unchanged insertion bounds are `(top,top)`, while existing open activation geometry reserves 26 px inside the 42 px body allowance. The dedicated lifetime scope gives those operands `top..top+26`. This preserves the independently alive empty sibling in the required ALT regression without changing insertion meaning.
|
||||
|
||||
Create capsules are 140×28, centered on the exact receiver lane and recorded occurrence row. The arrow ends on the sender-facing capsule edge. Delete X is centered on that lane/row. Both carry the original occurrence ID. Exact capsule/X shape hits precede named boxes and exact message-label rectangles; one shared nearest-distance arbitration across marker shapes and message strokes follows, using the existing 8-screen-pixel bound and persistent UUID tie breaking. Activation/guide hits remain the fallback. Neutral headers/guides retain the lifeline ID. Incomplete receiver-without-exchange records retain their explicit marker; receiver=None never borrows the global exchange endpoint.
|
||||
|
||||
Self lifecycle arrows end at the marker's recorded row, using a local elbow 24 px above it and 112 px to the right. Their row receives 16 px additional clearance **before** slot and row positions are recorded. This addresses the independent critic's Action/State/reference box overlap: the self caption at row−52 clears the previous box, whose bottom is previous-row+50. Ordinary Create captions are moved to row−40, above the capsule's top at row−14. No independent hidden coordinate is used for selection/reorder.
|
||||
|
||||
Activation geometry is intersected with **Alive intervals for the exact receiver and structural scope**. A local Delete ends that path's visible activation with an unresolved-call cue; each sibling is still visited independently. Post-fragment Absent/Unresolved intervals contain no definite activation rectangles or Reply caps. The call/reply records and correlation are unchanged. Root explicitly selected this scoped clipping over simply clipping at a Delete while leaving a solid bar in uncertain continuation.
|
||||
|
||||
## Independent criticism addressed before execution
|
||||
|
||||
The critic found a concrete minimum-Fit selection defect in the first rendering draft: at scale 0.05, adjacent Create/Delete rows 92 world pixels apart are only 4.6 screen pixels apart; the later Delete's expanded 8-pixel hit tolerance stole an exact birth-capsule center. The initial correction fixed exact centers/header hits but still let an earlier message's expanded stroke tolerance steal a practical off-X click. Root selected shared nearest-distance arbitration over merely moving marker halos ahead of all strokes. Final code compares both kinds under one bound and protects exact visible shapes and labels. Regression cases cover adjacent incomplete-receiver centers/header at 0.05/0.1/0.5/1/2, ±15/±25-world-pixel death offsets at minimum Fit, and an ordinary message genuinely nearer than a lifecycle halo. The independent reviewer read back this second revision and reported both counterexamples closed with no further confirmed material finding in the bounded source review. These were draft source-derived defects, not executed/native failures; the regressions have not yet run.
|
||||
|
||||
## Verification status
|
||||
|
||||
Executed: direct rustfmt over the four owned Rust files, `--edition 2024 --config skip_children=true`, exit 0; tracked `git diff --check`, exit 0. No Cargo, compiled test, real application rendering, screenshot or native acceptance is claimed by this worker. All new fixture helpers require ordinary `validate_candidate` and accepted `Document::from_bundle` before asserting Scene behavior; tests may not weaken admission to obtain desired drawings.
|
||||
|
||||
Ten new tests under `behavior::scenario_lifecycle::render::tests` exercise actual Scene construction:
|
||||
|
||||
1. Empty and Create-only root visible extents, unchanged insertion scopes, sender lifetime, and no lane masquerading as a message Line, OA/SA.
|
||||
2. Create→Action→Delete intervals, capsule/X endpoint alignment, caption clearance, marker/header/guide ID selection at 0.5×/1×/2×, OA/SA.
|
||||
3. Call→ALT(left Delete, right empty): left bar stops at death, right retains its own open bar/lifetime, post-fragment unresolved continuation has no definite bar, unanswered-call cues remain, no Reply is created, OA/SA.
|
||||
4. Warning-admitted unassigned lifeline with explicit incomplete Create receiver remains rendered/selectable while retaining authoring eligibility; missing receiver is not inferred, OA/SA.
|
||||
5. Adjacent Create/Delete capsule/X centers and neutral header keep original IDs at minimum Fit and four larger scales; off-X death offsets beat the earlier message tolerance, using an incomplete receiver.
|
||||
6. A genuinely nearer ordinary-message stroke wins over lifecycle marker halos at 0.05/0.1 scale.
|
||||
7. Explicit self Create/Delete endpoint/marker alignment with unresolved exchange, OA/SA. Admission remains a gate; no unsupported semantics may be forced if a fixture fails.
|
||||
8. Self Delete caption clears the immediately preceding action box and its slot still follows the recorded row.
|
||||
9. Actual ALT/Optional/Loop/Parallel geometry keeps lifecycle-bearing continuation unresolved and leaves unrelated sender lifetime definite.
|
||||
10. A meaningful Create→Asynchronous type edit changes lifetime geometry and source; one Document Undo and Redo restores exact accepted Project, complete authored CRLF/Unicode SourceBundle, marker/lifetime geometry and history availability, OA/SA.
|
||||
|
||||
Every ordinary Scene fixture also checks repeatable rendering, finite message coordinates, exact accepted Project/source snapshot preservation, no dirty/history/revision change. Existing ten pure projection tests retain their original scope; these Scene tests specifically address the three frozen independent integration findings.
|
||||
|
||||
## Handoff and remaining gates
|
||||
|
||||
Changed Rust paths: `crates/oa-desktop/src/behavior.rs`, `crates/oa-desktop/src/behavior/scenario_lifecycle.rs`, new `crates/oa-desktop/src/behavior/scenario_lifecycle/render.rs` and `render_tests.rs`. This report is the only new owned evidence path. The previous server audit report was separately corrected to classify its ff615d0 schema-label finding as already resolved by incoming c6 integration.
|
||||
|
||||
Root's first focused filters: `scenario_lifecycle`, then all `direct_canvas_tests` to cover existing message selection, slots, presses/releases, reorder, scrolling cancellation and activation regressions. Follow with relevant core scenario tests, complete desktop tests and strict Clippy. Independent source review and exact-binary native QA are required before accepting B4: minimum/wide layouts, long Unicode captions, both arrow directions, incomplete lanes, self arrows, marker/guide distinctions, nested operands, history/reopen and zoom/scroll input. The separate native fixture/oracle preserves exact complete source and explicit retained-file origin policy. No release acceptance follows from this implementation report.
|
||||
|
||||
## Separate built-in guide follow-up
|
||||
|
||||
Root subsequently assigned guide content only; Gitea ownership #189. Read the current Scenarios article and its existing creation, alternatives, interface operation and definition/use instructions before editing. Added two short explanation cards to `crates/oa-desktop/src/guide.rs`: reading lane headers/guides, solid lifetimes and Create/Delete capsule/X selection; distinguishing `?` path-dependent lifetime from unanswered-call `…` and inspecting lifecycle inside a referenced definition without inferring caller creation/deletion. Added lifecycle/birth/death/capsule search keywords. Existing sections and both branches' authoring/navigation instructions are unchanged. No renderer internals or new model-error claim is exposed.
|
||||
|
||||
Guide source freeze SHA256: `8D66EBF46BB088C24B03532593429EDC3DE34239F63D7D0D1D027DBE2C211926`. Scoped diff is 9 inserted lines and 1 replaced keyword line. Direct rustfmt of guide.rs only and `git diff --check` pass. No Cargo, tests, native validation, renderer/core/source change, language-spec change or global-setting mutation was performed in this follow-up. Root owns integration and actual guide/layout acceptance.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Independent REST/persistence review — 2026-09-11
|
||||
|
||||
Reviewer: Codex-ytr83r /root/server_audit. Reviewed checkout `ff615d0ee027997ea43d6d3bf95eabf946828561`; root is independently importing later authoring snapshots. Ownership: this report only. No production changes, Cargo/build commands, native input, live service, or filesystem fault injection. Coordination: Gitea issue #1 through #146, ownership #147, open PR inventory empty, Wiki Home and Authoring20260911.
|
||||
|
||||
## Verdict and ranked findings
|
||||
|
||||
**No concrete P0/P1/P2 implementation defect found** in the reviewed server persistence, conditional writes, source/model adoption, or restart admission paths. This is source review and limited static contract checking, not runtime release acceptance.
|
||||
|
||||
1. **P3 baseline documentation inconsistency — resolved by incoming integration.** At reviewed `ff615d0`, `docs/server.md:3` calls the shared model schema 5. Actual `crates/oa-core/src/model.rs:9` fixes `SCHEMA_VERSION` to 7, `docs/api.rst:159` says exports use schema/source 7, and OpenAPI `Project.properties.schema_version.const` is 7. Root confirmed incoming `c6d296a` already corrects this label to schema 7. No new edit is needed; this is a resolved baseline finding, not an outstanding defect in the integrated tree or a missing server feature.
|
||||
|
||||
No speculative cleanup findings are included. The gaps below concern verification, not verified product defects.
|
||||
|
||||
## Verified facts and falsifiers
|
||||
|
||||
| Guarantee traced | Evidence | Meaningful falsifier / existing test |
|
||||
| --- | --- | --- |
|
||||
| Final ETag comparison is serialized with commit, including no-op candidates | `crates/oa-server/src/store.rs:447` compares before `prepared.changed`; snapshot preparation at `store.rs:249` is outside the commit lock | Two candidates prepared from one ETag: exactly one changed commit, loser 412, only winner survives restart. Existing deterministic store test and `crates/oa-server/src/preparation_http_tests.rs:12` force the actual preparation interleaving. |
|
||||
| Pre-install write failure cannot adopt candidate memory | `store.rs:469` calls write before adoption at 470; temporary write/sync/persist at 510–515 | Force destination replacement failure in isolated store; assert unchanged ETag, complete model and source. Existing real HTTP tests at `tests/http.rs:1514` and `tests/http/source_projects.rs:167` do this; latter restores original disk bytes and checks restart/temp cleanup. |
|
||||
| Model and authored source are one atomic resource | `store.rs:144` uses command identity correspondence; replacement at 164 uses UUID correspondence; envelope serialization writes both; `PreparedProject::changed` compares both | Comment-only edit must advance ETag; stale semantic writer must lose. Existing `tests/http/source_projects.rs:684`; source no-op/restart at 92; identity swaps/dry-run at 262 and equal-model changed provenance at 359. |
|
||||
| Startup refuses inconsistent or unstabilized current-format pairs | `store.rs:270` opens/locks; regular-file check at 296, bounded read at 316, independent source/model comparison around 345–354 | Corrupt JSON, zero revision, filename/ID mismatch, absent persistent source IDs, model/source disagreement and second owner must fail without rewrite. Existing `tests/http.rs:1543` and `tests/http/source_projects.rs:736`. |
|
||||
| Post-install Unix sync failure has an uncertain response, while installed state and memory agree | `store.rs:470` adopts before directory sync at 471; `store.rs:521` documents that boundary; `error.rs:136` tells clients to reload | Inject only the post-install directory-sync failure: response 503, GET returns new complete pair/revision, restart returns same pair, stale original ETag refuses. No deterministic test for this specific failure was found. Ordinary replacement-failure tests do not cover it. |
|
||||
| HTTP timeout cannot release the worker permit while its blocking task continues | `crates/oa-server/src/lib.rs:114` has request timeout; `blocking` at 190 captures owned worker permit inside closure | Hold admitted work beyond deadline, observe 504, then release work: one complete outcome, no duplicate revision, stale ETag refuses, permits eventually recover. The existing preparation gate could support a focused test; no deadline-crossing test was found. This is an unexecuted falsifier, not a reported bug. |
|
||||
| Source7 nested identities and operations have service-level parity tests | `tests/http/source7_contracts.rs:189` covers authored bundle, typed edit, invalid/stale rejection and restart | Run the test against the integrated source; preserve full model, source bytes and identities through restart. Merely compiling does not establish this. |
|
||||
|
||||
Executed static checks: PowerShell parsed checked-in OpenAPI; **all 24 router paths exactly match its path inventory**; **all 489 local schema references resolve**. Confirmed OpenAPI current schema constant 7. These checks do not validate every command shape or execute the HTTP handlers.
|
||||
|
||||
## Remaining release evidence, in priority order
|
||||
|
||||
1. Root's serialized exact-source gates: `cargo test --locked -p oa-server`, `cargo clippy --locked -p oa-server --all-targets -- -D warnings`, and formatting. Prior Gitea/Wiki results belong to other explicitly identified checkpoints. This reviewer claims no test pass for this checkout or the eventual integrated checkout.
|
||||
2. Run `python crates/oa-server/tests/semantic_schema.py --check --validate` with jsonschema available, then `python crates/oa-server/tests/semantic_http_contract.py ABSOLUTE_PATH_TO_MATCHING_SERVER_BINARY`. The latter compares served and checked-in OpenAPI and validates actual responses against both. No Python command was available through this review shell's PATH lookup, so these were not executed here.
|
||||
3. Fresh native Linux current-source suite and optimized binary smoke: temporary local store; token/auth, exclusive ownership, conditional write and restart, paired source/model exports, delete and restart, SIGTERM drain. `docs/server.md:147` expressly limits prior Linux evidence to the old V2 baseline. Container build/run remains separate when Docker deployment is claimed (`docs/server.md:149`).
|
||||
4. Add or run bounded controlled-failure probes for the two documented but uncovered boundaries above: HTTP cancellation/deadline after admission, and Unix directory-sync failure after installation. Do not weaken assertions to pre-install rollback after a post-install fault. Both must assert complete-pair coherence and honest unknown outcome; power-loss survival is explicitly outside the documented guarantee.
|
||||
|
||||
The documented exclusions are coherent with implementation: one local store owner; no out-of-band file synchronization; current revision only; no server undo, idempotency receipt or automatic retry of uncertain create/import. No evidence here justifies inventing those capabilities or marking their deliberate absence a defect. Root retains integration, any repairs, runtime verification and final release verdict.
|
||||
Reference in New Issue
Block a user