Extend Scene layout experiment with lifetime invariants
This commit is contained in:
@@ -57,3 +57,11 @@ Root should publish this directory and the exact wired code/test result together
|
||||
The panic-restoration evaluator originally accepted any panic inside its scope, so an earlier failed geometry/thread assertion could have looked like successful deliberate unwinding. Root corrected it to require the exact deliberate panic payload. Moved message polylines now also require equal point counts before zipped endpoint comparisons. These are evaluator corrections; execution remains pending.
|
||||
|
||||
Independent bounded review confirmed hook placement, fixture assumptions and the hand-derived coordinates without executing them. Its coverage caveat prompted adding box marker metadata and insertion-slot labels to equality comparisons. Transient selection, camera and interaction state are not part of this geometry comparison. No material source finding remains from that review; compiler/runtime evidence is still required.
|
||||
|
||||
## Peer activation integration boundary
|
||||
|
||||
A read-only check of peer source `8112dea7a371755c7373f4ecc09a6488a73ac136` found that the pre-row hook remains upstream of typed activation construction and final message anchoring. That anchoring preserves generated y coordinates. Its column-zero self-call changes x from the old lane/lane+75/lane+75/lane path to lane/lane+66/lane+66/lane+6; receive y remains660 in this fixture, and the activation begins there rather than at send y636.
|
||||
|
||||
The peer branch and this branch have different lifetime integration states. This branch has production lifetime rendering, message alignment, self-lifecycle clearance and alive_spans clipping. In the peer checkpoint, scenario_lifecycle is cfg(test)-only and typed activation_segment emits an unclipped span. A future union must retain the lifetime behavior and reconcile duplicate activation methods; copying either whole Scene implementation is insufficient. Compare both lifecycle geometry and typed segment identity/rectangle/column/finish, and verify delayed Create/Delete inside and outside operands. Do not remove the existing lifecycle comparison to make the peer version compile. A segment clipped before its semantic reply must not falsely claim a visible reply termination. No peer code has been merged by this experiment.
|
||||
|
||||
[Shared integration finding](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/21#issuecomment-698) and [peer activation source](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/8112dea7a371755c7373f4ecc09a6488a73ac136/crates/oa-desktop/src/behavior/scenario_activation.rs). This is source compatibility evidence, not runtime acceptance.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
arcadia 7;
|
||||
// Separate lifetime probe: preserve this authored note and all stable IDs.
|
||||
project lifetime_probe "Delayed receiver lifetime" id "20000000-0000-4000-8000-000000000001" {
|
||||
oa_activity prepare "Prepare request" id "20000000-0000-4000-8000-000000000002" {}
|
||||
oa_activity serve "Serve request" id "20000000-0000-4000-8000-000000000003" {}
|
||||
interaction service "Service request" id "20000000-0000-4000-8000-000000000004" {
|
||||
source @prepare; target @serve;
|
||||
}
|
||||
oa_scenario lifetime_conversation "Receiver created on demand" id "20000000-0000-4000-8000-000000000005" {
|
||||
viewpoint OperationalActivity;
|
||||
steps [@before_birth, @create_receiver, @service_call, @delete_receiver];
|
||||
}
|
||||
oa_lifeline requester "Requester" id "20000000-0000-4000-8000-000000000006" {
|
||||
parent @lifetime_conversation; represented @prepare;
|
||||
}
|
||||
oa_lifeline provider "Provider" id "20000000-0000-4000-8000-000000000007" {
|
||||
parent @lifetime_conversation; represented @serve;
|
||||
}
|
||||
oa_occurrence before_birth "Prepare before receiver exists" id "20000000-0000-4000-8000-000000000008" {
|
||||
parent @lifetime_conversation;
|
||||
body { Action { action @prepare; lifeline @requester; }; };
|
||||
}
|
||||
oa_occurrence create_receiver "Create receiver" id "20000000-0000-4000-8000-000000000009" {
|
||||
parent @lifetime_conversation;
|
||||
body { Exchange {
|
||||
exchange @service; sender @requester; receiver @provider;
|
||||
message_kind Create;
|
||||
}; };
|
||||
}
|
||||
oa_occurrence service_call "Call receiver" id "20000000-0000-4000-8000-00000000000a" {
|
||||
parent @lifetime_conversation;
|
||||
body { Exchange {
|
||||
exchange @service; sender @requester; receiver @provider;
|
||||
message_kind Call;
|
||||
}; };
|
||||
}
|
||||
// Death ends the visible activation but deliberately does not invent a Reply.
|
||||
oa_occurrence delete_receiver "Delete receiver" id "20000000-0000-4000-8000-00000000000b" {
|
||||
parent @lifetime_conversation;
|
||||
body { Exchange {
|
||||
exchange @service; sender @requester; receiver @provider;
|
||||
message_kind Delete;
|
||||
}; };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# Lifetime extension of the actual Scene experiment — not executed
|
||||
|
||||
Prepared 2026-09-12 in `codex/behavior-layout-scene-ytr83r-20260912`, inspected
|
||||
HEAD `911f63e`. This extension owns only `lifetime.arcadia`, `lifetime.rs`, this
|
||||
note, and the `mod lifetime;` declaration in the existing experiment module.
|
||||
The original fixture and its seven baseline tests are unchanged. Root owns the
|
||||
README compatibility note, compiler resource, source integration and publication.
|
||||
No production renderer, parser or model code was edited here.
|
||||
|
||||
## Question and method
|
||||
|
||||
Does the current test-only row-anchor hook propagate a changed Create/Delete
|
||||
position into the actual Scene's local lifetime, marker hit identity, aligned
|
||||
message endpoints and activation clipping, while leaving semantic/source state
|
||||
and occurrence IDs intact?
|
||||
|
||||
The separate source fixture declares stable UUIDs, a sender action before receiver
|
||||
birth, then Create, Call and receiver Delete. All three messages reference one
|
||||
valid Interaction; explicit sender/receiver lifelines represent its Activity
|
||||
endpoints. A Reply is deliberately absent: receiver death must not manufacture a
|
||||
reply or erase the unanswered-call presentation cue. The test requires real
|
||||
language parsing, no semantic errors, ordinary `Document::from_bundle` admission,
|
||||
canonical format/reparse Project equality and exact original source retention.
|
||||
Warnings are allowed and are not promoted into semantic acceptance requirements.
|
||||
|
||||
The evaluator calls the existing `Scene::sequence` and scoped `render`/`drag`
|
||||
helpers. It reads `Rendering::alive_spans` and `marker_at`, real Scene line
|
||||
polylines and actual activation `BoxItem` records. It does not construct an
|
||||
alternative activation/lifetime implementation or edit final geometry afterward.
|
||||
|
||||
## Independent expectations
|
||||
|
||||
Current Scene metrics give sender action148, Create243, Call335 and Delete427:
|
||||
the action consumes95 and each following message92. The scene height is564 and
|
||||
its root insertion bounds are106–477. These numbers are regression oracles for
|
||||
the inspected implementation, not permanent product spacing requirements.
|
||||
|
||||
The receiver is alive exactly243–427; the sender remains alive throughout the
|
||||
visible root span. Create ends at the 140px birth capsule's near edge, 70px before
|
||||
the receiver lane, while Delete ends at the death cross's center. Marker and
|
||||
Scene hit queries return the original Create/Delete occurrence IDs. The actual
|
||||
8px activation starts at Call335 and ends at death427, with no minimum-height tail
|
||||
after death. The unresolved-call cue stays at receiver-x+12, death-y−16.
|
||||
|
||||
Three tests establish these expectations if they pass:
|
||||
|
||||
1. Baseline parsed Scene has those independent rows, line endpoints, lifetime
|
||||
extents, hit identities, activation bounds and unanswered-call cue. Its real
|
||||
geometry remains equal after source round trip and an empty hook scope.
|
||||
2. Moving Create down60 changes its row to303 and propagates Call395/Delete487;
|
||||
the actual receiver lifetime, birth/death markers and activation follow. The
|
||||
preceding action's box stays fixed, line/row UUIDs stay stable, and the old
|
||||
birth point is no longer a lifecycle marker.
|
||||
3. Moving only Delete down48 changes death to475 and extends the same real
|
||||
activation to475, while Create and Call polylines stay fixed. Moving Create
|
||||
with an authored Delete request still at427 forces displayed death487; either
|
||||
signed-zero pointer delta preserves the entire latent map and Scene. Moving
|
||||
Create back restores baseline geometry and the original authored map.
|
||||
|
||||
Every test compares the entire accepted Project and source bundle against the
|
||||
original snapshot, and requires no dirty state or undo/redo history. No test
|
||||
weakens equality to just the occurrence list.
|
||||
|
||||
## Contract reused and limits
|
||||
|
||||
The inspected implementation builds lifecycle geometry after recording occurrence
|
||||
rows (`behavior.rs:1332–1344`), aligns Create/Delete messages through
|
||||
`scenario_lifecycle::render::Rendering::align_messages`, and constructs actual
|
||||
activation boxes through `activation_sequence`/`activation_segment`
|
||||
(`behavior.rs:1663–1734`). The latter intersects segments with `alive_spans` and
|
||||
keeps a cue when Delete terminates an unanswered call. Existing
|
||||
`scenario_lifecycle/render_tests.rs` tests real root birth/death alignment and
|
||||
branch-local activation clipping. This extension exercises those current
|
||||
contracts through the layout hook; it does not replace them.
|
||||
|
||||
The peer `8112dea` typed activation correction has a different future integration
|
||||
boundary and, as reported to this task, has no production lifecycle clipping yet.
|
||||
These tests are not evidence that that unmerged source is compatible. A future
|
||||
typed activation union needs its own tests for clipping, typed call/segment IDs,
|
||||
self/nested activation receive geometry, branch behavior, depth and paint order.
|
||||
Do not preserve a legacy width/classification assertion blindly when deliberately
|
||||
changing the activation representation; retain independent lifetime endpoints,
|
||||
identity and no-after-death expectations on the new actual records.
|
||||
|
||||
This extension remains uncompiled and unexecuted. It does not establish persisted
|
||||
layout syntax, a production drag transaction, undoable geometry edits, native
|
||||
pointer behavior, GPU painting, performance, or general upward/free positioning.
|
||||
The existing hook still applies a next-row floor. Root should run the existing
|
||||
`cargo test -p oa-desktop behavior_layout_scene_experiment -- --nocapture` gate
|
||||
on the exact identified wired source, then publish fixture, evaluator and result
|
||||
together. Remote peer machines cannot use workspace-local links.
|
||||
|
||||
Independent bounded source review confirmed the fixture ownership/endpoint contract and the numerical expectations above. Formatting and whitespace checks passed. Compilation and execution remain pending; these findings do not establish an editing transaction or native behavior.
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Real current Scene/lifecycle evaluator, independent of the original fixture.
|
||||
//! No replacement activation geometry or future peer implementation is used.
|
||||
use super::*;
|
||||
use crate::document::Document;
|
||||
use oa_core::source_project::{ProjectLimits, SourceBundle};
|
||||
|
||||
fn lifetime_document() -> (Document, Id) {
|
||||
let source = include_str!("lifetime.arcadia");
|
||||
let parsed = language::parse(source).expect("separate lifetime source parses");
|
||||
let errors: Vec<_> = validation::validate(&parsed)
|
||||
.into_iter()
|
||||
.filter(|issue| issue.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"lifetime fixture semantic errors: {errors:?}"
|
||||
);
|
||||
let owner = id(&parsed, "lifetime_conversation");
|
||||
let bundle = SourceBundle {
|
||||
entry: "lifetime.arcadia".into(),
|
||||
files: BTreeMap::from([("lifetime.arcadia".into(), source.into())]),
|
||||
};
|
||||
let document = Document::from_bundle(bundle.clone(), ProjectLimits::default())
|
||||
.expect("lifetime fixture is ordinarily admitted");
|
||||
assert_eq!(document.project(), &parsed);
|
||||
assert_eq!(document.bundle(), &bundle);
|
||||
assert_eq!(language::parse(&language::format(&parsed)).unwrap(), parsed);
|
||||
(document, owner)
|
||||
}
|
||||
|
||||
/// Numeric expectations are hand-derived current-Scene metrics, not proposed
|
||||
/// permanent spacing rules: Action95, message92, initial occurrence148.
|
||||
fn assert_lifetime_geometry(
|
||||
scene: &Scene,
|
||||
project: &Project,
|
||||
owner: Id,
|
||||
birth: f32,
|
||||
call_y: f32,
|
||||
death: f32,
|
||||
) {
|
||||
let requester = id(project, "requester");
|
||||
let provider = id(project, "provider");
|
||||
let create = id(project, "create_receiver");
|
||||
let call = id(project, "service_call");
|
||||
let delete = id(project, "delete_receiver");
|
||||
let left = scene.lanes[&requester];
|
||||
let right = scene.lanes[&provider];
|
||||
assert_eq!((left, right), (160., 400.));
|
||||
assert_eq!(row(scene, id(project, "before_birth")), 148.);
|
||||
assert_eq!(row(scene, create), birth);
|
||||
assert_eq!(row(scene, call), call_y);
|
||||
assert_eq!(row(scene, delete), death);
|
||||
assert!(
|
||||
birth > 148.,
|
||||
"receiver creation is delayed beyond the opening action"
|
||||
);
|
||||
assert_eq!(scene.rows[&create].0, owner);
|
||||
assert_eq!(scene.rows[&delete].0, owner);
|
||||
|
||||
// Exercise the actual Rendering bridge, including marker hit identity and
|
||||
// its clipping contract, rather than rebuilding lifetimes from our rows.
|
||||
let alive = scene
|
||||
.lifecycle
|
||||
.alive_spans(provider, owner, 85., scene.size.height - 20.);
|
||||
assert_eq!(
|
||||
alive
|
||||
.iter()
|
||||
.map(|span| (span.start, span.end))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(birth, death)]
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.lifecycle
|
||||
.alive_spans(provider, owner, 85., birth)
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.lifecycle
|
||||
.alive_spans(provider, owner, death, scene.size.height - 20.)
|
||||
.is_empty()
|
||||
);
|
||||
let sender_alive = scene
|
||||
.lifecycle
|
||||
.alive_spans(requester, owner, 85., scene.size.height - 20.);
|
||||
assert_eq!(
|
||||
sender_alive
|
||||
.iter()
|
||||
.map(|span| (span.start, span.end))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(85., scene.size.height - 20.)]
|
||||
);
|
||||
assert_eq!(
|
||||
scene.lifecycle.marker_at(Point::new(right, birth)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.lifecycle.marker_at(Point::new(right, death)),
|
||||
Some(delete)
|
||||
);
|
||||
assert_eq!(scene.hit(Point::new(right, birth)), Some(create));
|
||||
assert_eq!(scene.hit(Point::new(right, death)), Some(delete));
|
||||
assert_eq!(
|
||||
line(scene, create).points,
|
||||
vec![Point::new(left, birth), Point::new(right - 70., birth)]
|
||||
);
|
||||
assert_eq!(
|
||||
line(scene, call).points,
|
||||
vec![Point::new(left, call_y), Point::new(right, call_y)]
|
||||
);
|
||||
assert_eq!(
|
||||
line(scene, delete).points,
|
||||
vec![Point::new(left, death), Point::new(right, death)]
|
||||
);
|
||||
assert!(line(scene, create).label_at.y + 22. < birth - 14.);
|
||||
|
||||
// Inspect actual activation boxes produced by Scene::activation_sequence.
|
||||
// A correlated Reply is absent, so death must clip the bar, not close the
|
||||
// semantic call. Do not accept a new generic minimum-height tail after death.
|
||||
let bars: Vec<_> = scene
|
||||
.boxes
|
||||
.iter()
|
||||
.filter(|item| item.id == call && item.title.is_empty())
|
||||
.collect();
|
||||
assert_eq!(bars.len(), 1);
|
||||
assert_eq!(
|
||||
bars[0].rect,
|
||||
Rectangle::new(
|
||||
Point::new(right - 4., call_y),
|
||||
Size::new(8., death - call_y)
|
||||
)
|
||||
);
|
||||
assert_eq!(bars[0].rect.y + bars[0].rect.height, death);
|
||||
assert!(
|
||||
bars.iter()
|
||||
.all(|item| item.rect.y >= birth && item.rect.y + item.rect.height <= death)
|
||||
);
|
||||
assert_eq!(scene.open_calls, vec![Point::new(right + 12., death - 16.)]);
|
||||
assert!(!project.elements.values().any(|element| matches!(
|
||||
element.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Reply,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_create_call_and_delete_have_real_lifetime_markers_and_exact_clipped_activation() {
|
||||
let (document, owner) = lifetime_document();
|
||||
let before = document.snapshot();
|
||||
let source = document.bundle().clone();
|
||||
let scene = Scene::sequence(document.project(), owner, owner);
|
||||
assert_lifetime_geometry(&scene, document.project(), owner, 243., 335., 427.);
|
||||
assert_eq!(scene.size.height, 564.);
|
||||
assert_eq!(scene.sequence_bounds[&owner], (106., 477.));
|
||||
same_geometry(&scene, &render(document.project(), owner, &Layout::new()));
|
||||
let reparsed = language::parse(&language::format(document.project())).unwrap();
|
||||
same_geometry(&scene, &Scene::sequence(&reparsed, owner, owner));
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &source);
|
||||
assert!(!document.dirty() && !document.can_undo() && !document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_create_moves_real_lifetime_and_followers_without_changing_semantics_or_source() {
|
||||
let (document, owner) = lifetime_document();
|
||||
let before = document.snapshot();
|
||||
let source = document.bundle().clone();
|
||||
let p = document.project();
|
||||
let create = id(p, "create_receiver");
|
||||
let provider = id(p, "provider");
|
||||
let baseline = Scene::sequence(p, owner, owner);
|
||||
let layout = drag(&Layout::new(), &baseline, create, 60.);
|
||||
assert_eq!(layout, Layout::from([(create, 303.)]));
|
||||
let moved = render(p, owner, &layout);
|
||||
assert_lifetime_geometry(&moved, p, owner, 303., 395., 487.);
|
||||
assert_eq!(moved.size.height, 624.);
|
||||
assert_eq!(
|
||||
moved
|
||||
.lifecycle
|
||||
.marker_at(Point::new(moved.lanes[&provider], 243.)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
named_box(&moved, id(p, "before_birth")).rect,
|
||||
named_box(&baseline, id(p, "before_birth")).rect
|
||||
);
|
||||
assert_eq!(
|
||||
moved.rows.keys().collect::<Vec<_>>(),
|
||||
baseline.rows.keys().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
moved.lines.iter().map(|line| line.id).collect::<Vec<_>>(),
|
||||
baseline
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| line.id)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let reparsed = language::parse(&language::format(p)).unwrap();
|
||||
same_geometry(&moved, &render(&reparsed, owner, &layout));
|
||||
assert_eq!(p, &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &source);
|
||||
assert!(!document.dirty() && !document.can_undo() && !document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_delete_extends_only_to_actual_death_and_zero_motion_preserves_latent_lifetime_intent() {
|
||||
let (document, owner) = lifetime_document();
|
||||
let before = document.snapshot();
|
||||
let source = document.bundle().clone();
|
||||
let p = document.project();
|
||||
let create = id(p, "create_receiver");
|
||||
let delete = id(p, "delete_receiver");
|
||||
let provider = id(p, "provider");
|
||||
let baseline = Scene::sequence(p, owner, owner);
|
||||
let later_delete = drag(&Layout::new(), &baseline, delete, 48.);
|
||||
let extended = render(p, owner, &later_delete);
|
||||
assert_lifetime_geometry(&extended, p, owner, 243., 335., 475.);
|
||||
assert_eq!(extended.size.height, 612.);
|
||||
assert_eq!(
|
||||
extended
|
||||
.lifecycle
|
||||
.marker_at(Point::new(extended.lanes[&provider], 427.)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
line(&extended, create).points,
|
||||
line(&baseline, create).points
|
||||
);
|
||||
assert_eq!(
|
||||
line(&extended, id(p, "service_call")).points,
|
||||
line(&baseline, id(p, "service_call")).points
|
||||
);
|
||||
|
||||
// Create displacement forces the displayed death to487 while its authored
|
||||
// request stays427. Both signed zero deltas must preserve that latent value.
|
||||
let original = Layout::from([(create, 243.), (delete, 427.)]);
|
||||
let displaced = drag(&original, &baseline, create, 60.);
|
||||
let frame = render(p, owner, &displaced);
|
||||
assert_lifetime_geometry(&frame, p, owner, 303., 395., 487.);
|
||||
assert_eq!(displaced[&delete], 427.);
|
||||
for delta in [0., -0.] {
|
||||
let untouched = drag(&displaced, &frame, delete, delta);
|
||||
assert_eq!(untouched, displaced);
|
||||
same_geometry(&frame, &render(p, owner, &untouched));
|
||||
let restored = drag(&untouched, &frame, create, -60.);
|
||||
assert_eq!(restored, original);
|
||||
same_geometry(&baseline, &render(p, owner, &restored));
|
||||
}
|
||||
assert_eq!(p, &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &source);
|
||||
assert!(!document.dirty() && !document.can_undo() && !document.can_redo());
|
||||
}
|
||||
@@ -4,6 +4,8 @@ use super::*;
|
||||
use oa_core::{language, validation};
|
||||
use std::cell::RefCell;
|
||||
|
||||
mod lifetime;
|
||||
|
||||
type Layout = BTreeMap<Id, f32>;
|
||||
|
||||
thread_local! {
|
||||
|
||||
Reference in New Issue
Block a user