Compare commits

...
90 changed files with 17319 additions and 19 deletions
+60 -4
View File
@@ -10,6 +10,7 @@ mod hosted;
mod reference_authoring;
#[cfg(test)]
mod review_navigation_tests;
pub(crate) mod source_rename;
mod source_symbols;
#[cfg(test)]
mod transition_context_tests;
@@ -436,6 +437,8 @@ pub enum Message {
crate::source_navigation::SymbolEvent,
),
SourceSymbolsReady(crate::source_navigation::Completion),
SourceRename(source_rename::Event),
SourceRenameReady(source_rename::Completion),
DiagramUsage(crate::diagram_usage::Event),
DiagramUsagesReady(crate::diagram_usage::Completion),
DiagramRevealViewport(crate::diagram_usage::Context, u64, iced::Size),
@@ -521,6 +524,7 @@ pub struct App {
pub source_navigation: crate::source_navigation::State,
source_find_instance: Id,
pub source_symbols: crate::source_navigation::SymbolState,
pub source_rename: source_rename::State,
pub(crate) diagram_usages: Option<crate::diagram_usage::Panel>,
pub(crate) diagram_navigation_focus: Option<crate::diagram_usage::Focus>,
source_diagram_job: Option<Id>,
@@ -640,6 +644,7 @@ impl App {
source_navigation: crate::source_navigation::State::default(),
source_find_instance: Id::new_v4(),
source_symbols: crate::source_navigation::SymbolState::default(),
source_rename: source_rename::State::default(),
diagram_usages: None,
diagram_navigation_focus: None,
source_diagram_job: None,
@@ -691,19 +696,37 @@ impl App {
|| self.behavior.has_pending()
}
fn sync_source(&mut self) {
self.sync_source_with_alias_editor(None);
}
fn sync_source_with_alias_editor(&mut self, hint: Option<source_rename::EditorRefresh<'_>>) {
self.source_workspace.accept(self.document.bundle());
self.show_source_file(false);
self.show_source_file_with_alias_editor(false, hint);
self.source_diagnostics = self.document.compiled().diagnostics.clone();
self.source_version += 1;
self.lint_requested = None;
}
fn show_source_file(&mut self, restore_cursor: bool) {
self.show_source_file_with_alias_editor(restore_cursor, None);
}
fn show_source_file_with_alias_editor(
&mut self,
restore_cursor: bool,
hint: Option<source_rename::EditorRefresh<'_>>,
) {
self.source_symbols.reveal = None;
self.source_tree.reveal(&self.source_workspace.active);
self.source_select_line = false;
let source = self.source_workspace.text().to_owned();
self.source_navigation.refresh(&source);
self.source_editor = text_editor::Content::with_text(&source);
if !hint.is_some_and(|hint| {
hint.apply(
&mut self.source_editor,
&self.source_workspace.active,
&source,
)
}) {
self.source_editor = text_editor::Content::with_text(&source);
}
self.source_snapshot = Arc::from(source);
self.source_jump = restore_cursor
.then(|| {
@@ -748,6 +771,9 @@ impl App {
}
}
fn sync(&mut self) {
self.sync_with_alias_editor(None);
}
fn sync_with_alias_editor(&mut self, hint: Option<source_rename::EditorRefresh<'_>>) {
self.explorer_index = crate::explorer::Index::new(self.project(), self.layer);
self.model_issues = validate(self.project());
self.normalize_creation();
@@ -777,7 +803,7 @@ impl App {
.map(|d| d.id);
}
self.refresh_draft();
self.sync_source();
self.sync_source_with_alias_editor(hint);
}
fn normalize_creation(&mut self) {
if !self.create_kind.supports(self.layer) {
@@ -970,6 +996,7 @@ impl App {
let source_version = self.source_version + 1;
let lint_running = self.lint_running;
let navigation_in_flight = self.source_symbols.in_flight;
let rename_in_flight = self.source_rename.in_flight;
let diagram_lookup_in_flight = self.diagram_lookup_in_flight;
let guide = std::mem::take(&mut self.guide);
let mut hosted = std::mem::take(&mut self.hosted);
@@ -983,6 +1010,7 @@ impl App {
self.source_version = source_version;
self.lint_running = lint_running;
self.source_symbols.in_flight = navigation_in_flight;
self.source_rename.in_flight = rename_in_flight;
self.diagram_lookup_in_flight = diagram_lookup_in_flight;
self.guide = guide;
self.path = path;
@@ -1146,7 +1174,9 @@ impl App {
|| self.diagram_usages.is_some()
|| self.behavior.source_active()
|| (self.page == Page::Text
&& (self.source_symbols.open || self.source_navigation.open)
&& (self.source_symbols.open
|| self.source_navigation.open
|| self.source_rename.open())
&& self.architecture.is_none()),
)
.map(|()| Message::Escape),
@@ -1194,6 +1224,13 @@ impl App {
])
}
pub fn update(&mut self, message: Message) -> Task<Message> {
// Reject stale rename intents before camera/reveal reconciliation or
// source interaction can disturb a newer navigation destination.
if let Message::SourceRename(event) = &message
&& !self.source_rename_envelope(event)
{
return Task::none();
}
let usage_viewport = self.diagram_reveal_viewport(&message);
let window_resized = matches!(&message, Message::WindowResized(_));
// Reject stale measurements before legacy viewport handlers can change
@@ -1346,6 +1383,9 @@ impl App {
if let Message::SourceSymbolsReady(completion) = message {
return self.source_symbols_ready(completion);
}
if let Message::SourceRenameReady(completion) = message {
return self.source_rename_ready(completion);
}
if let Message::DiagramUsagesReady(completion) = message {
return self.diagram_usages_ready(completion);
}
@@ -1567,6 +1607,18 @@ impl App {
{
return Task::none();
}
if self.source_rename.open() {
if matches!(
message,
Message::Escape | Message::Request(Pending::Close(_))
) {
self.close_source_rename();
return Task::none();
}
if !modal_event && !matches!(message, Message::SourceRename(_)) {
return Task::none();
}
}
if matches!(
message,
Message::Page(_) | Message::Layer(_) | Message::Diagram(_)
@@ -1611,6 +1663,7 @@ impl App {
self.last_workspace_page = self.page;
}
let mut task = self.handle(message);
self.refresh_source_rename_context();
self.reconcile_reference_chooser();
self.route_state_diagram_workspace();
if self.page != Page::Home {
@@ -3210,6 +3263,7 @@ impl App {
if self.pending_recovery.as_ref() != Some(&receipt.target) {
return Task::none();
}
self.invalidate_source_rename("Disk recovery changed this source context. Your replacement is retained; close and invoke Rename again.");
self.pending_recovery = None;
self.source_diagnostics
.retain(|issue| issue.diagnostic.code != "pending_save");
@@ -3297,6 +3351,8 @@ impl App {
Message::SourceViewport(ticket) => return self.source_viewport(ticket),
Message::SourceFindClose(..) => return Task::none(), // update checks and unwraps it
Message::SourceSymbols(_, event) => return self.source_symbol_event(event),
Message::SourceRename(event) => return self.source_rename_event(event),
Message::SourceRenameReady(completion) => return self.source_rename_ready(completion),
Message::SourceSymbolsReady(completion) => {
return self.source_symbols_ready(completion);
}
@@ -85,6 +85,7 @@ impl App {
| Message::DiagramUsage(_)
| Message::DiagramUsagesReady(_)
| Message::SourceSymbolsReady(_)
| Message::SourceRenameReady(_)
| Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(..))
| Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Viewport(..))
| Message::StateDiagram(behavior::state_edit::Event::Viewport(..))
@@ -15,6 +15,7 @@ impl App {
fn usage_navigation_available(&self) -> bool {
!self.busy
&& !self.source_rename.open()
&& self.pending.is_none()
&& self.file_prompt.is_none()
&& !self.delete_pending
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,219 @@
//! Keep Iced's single-line input from silently filtering pasted control bytes.
//! Only the alias field uses this adapter. The child retains editing and focus.
use iced::advanced::{
Clipboard, Layout, Shell, Widget, clipboard, layout, overlay, renderer, text, widget,
};
use iced::{Element, Event, Length, Rectangle, Size, Vector, keyboard, mouse};
pub(super) fn guard<'a, Message: Clone + 'a, Theme: 'a, R: text::Renderer + 'a>(
input: Element<'a, Message, Theme, R>,
rejected: Option<Message>,
) -> Element<'a, Message, Theme, R> {
Element::new(AliasInput { input, rejected })
}
struct AliasInput<'a, Message, Theme, R> {
input: Element<'a, Message, Theme, R>,
rejected: Option<Message>,
}
#[derive(Default)]
struct State {
modifiers: keyboard::Modifiers,
}
// Each fresh child clipboard read uses the checked snapshot. Iced may reuse its
// previously admitted paste while V is held, until KeyReleased clears that cache.
struct CapturedClipboard<'a> {
content: Option<String>,
original: &'a mut dyn Clipboard,
}
impl Clipboard for CapturedClipboard<'_> {
fn read(&self, kind: clipboard::Kind) -> Option<String> {
if kind == clipboard::Kind::Standard {
self.content.clone()
} else {
self.original.read(kind)
}
}
fn write(&mut self, kind: clipboard::Kind, contents: String) {
self.original.write(kind, contents);
}
}
impl<Message: Clone, Theme, R: text::Renderer> Widget<Message, Theme, R>
for AliasInput<'_, Message, Theme, R>
{
fn size(&self) -> Size<Length> {
self.input.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.input.as_widget().size_hint()
}
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<State>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State::default())
}
fn children(&self) -> Vec<widget::Tree> {
vec![widget::Tree::new(self.input.as_widget())]
}
fn diff(&self, tree: &mut widget::Tree) {
tree.diff_children(std::slice::from_ref(&self.input));
}
fn layout(
&mut self,
tree: &mut widget::Tree,
renderer: &R,
limits: &layout::Limits,
) -> layout::Node {
self.input
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.input.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn operate(
&mut self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn widget::Operation,
) {
self.input
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut widget::Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let focused = tree.children[0]
.state
.downcast_ref::<iced::widget::text_input::State<R::Paragraph>>()
.is_focused();
let state = tree.state.downcast_mut::<State>();
// Iced 0.14.2 uses stored ModifiersChanged state, not KeyPressed.modifiers.
// Mirror its Escape reset and its Latin/physical-key paste shortcut.
if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event {
state.modifiers = *modifiers;
}
let paste = matches!(event,
Event::Keyboard(keyboard::Event::KeyPressed { key, physical_key, .. })
if key.to_latin(*physical_key) == Some('v')
&& state.modifiers.command() && !state.modifiers.alt());
if focused
&& paste
&& let Some(rejected) = &self.rejected
{
let content = clipboard.read(clipboard::Kind::Standard);
if content
.as_ref()
.is_some_and(|value| value.chars().any(char::is_control))
{
// Do not invoke the child: even an empty replacement would erase
// its selection. Approval invalidation is an explicit App event.
shell.publish(rejected.clone());
shell.capture_event();
return;
}
self.input.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
&mut CapturedClipboard {
content,
original: clipboard,
},
shell,
viewport,
);
return;
}
self.input.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
// In Iced 0.14.2 the only KeyPressed branch that removes focus is its
// Escape branch, which also resets stored modifiers. Observe that branch
// after its text/shortcut early returns and effective-key conversion.
if focused
&& matches!(event, Event::Keyboard(keyboard::Event::KeyPressed { .. }))
&& !tree.children[0]
.state
.downcast_ref::<iced::widget::text_input::State<R::Paragraph>>()
.is_focused()
{
tree.state.downcast_mut::<State>().modifiers = keyboard::Modifiers::default();
}
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.input.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'a>(
&'a mut self,
tree: &'a mut widget::Tree,
layout: Layout<'a>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'a, Message, Theme, R>> {
self.input.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
#[cfg(all(test, debug_assertions))]
mod tests;
@@ -0,0 +1,220 @@
use super::*;
use crate::views::source_tests::FontLayout;
use iced::keyboard::{
Key, Modifiers,
key::{Code, Named, Physical},
};
use std::cell::Cell;
#[derive(Debug, Clone, PartialEq)]
enum Signal {
Input(String),
Rejected,
}
struct ClipboardText {
text: String,
reads: Cell<usize>,
}
impl Clipboard for ClipboardText {
fn read(&self, _: clipboard::Kind) -> Option<String> {
self.reads.set(self.reads.get() + 1);
Some(self.text.clone())
}
fn write(&mut self, _: clipboard::Kind, _: String) {}
}
type InputState = iced::widget::text_input::State<<FontLayout as text::Renderer>::Paragraph>;
struct Mounted {
input: Element<'static, Signal, iced::Theme, FontLayout>,
tree: widget::Tree,
node: layout::Node,
clipboard: ClipboardText,
messages: Vec<Signal>,
}
impl Mounted {
fn new(focused: bool, enabled: bool, modifiers: Modifiers) -> Self {
let mut child = iced::widget::text_input("Alias", "send_water");
if enabled {
child = child.on_input(Signal::Input);
}
let mut input = guard(child.into(), enabled.then_some(Signal::Rejected));
let mut tree = widget::Tree::new(input.as_widget());
let node = input.as_widget_mut().layout(
&mut tree,
&FontLayout,
&layout::Limits::new(Size::ZERO, Size::new(500., 40.)),
);
if focused {
let state = tree.children[0].state.downcast_mut::<InputState>();
state.focus();
state.select_range(0, 10);
}
let mut mounted = Self {
input,
tree,
node,
clipboard: ClipboardText {
text: "foo\nbar".into(),
reads: Cell::new(0),
},
messages: vec![],
};
mounted.event(Event::Keyboard(keyboard::Event::ModifiersChanged(
modifiers,
)));
mounted
}
fn state(&self) -> &InputState {
self.tree.children[0].state.downcast_ref::<InputState>()
}
fn event(&mut self, event: Event) -> bool {
let mut shell = Shell::new(&mut self.messages);
self.input.as_widget_mut().update(
&mut self.tree,
&event,
Layout::new(&self.node),
mouse::Cursor::Unavailable,
&FontLayout,
&mut self.clipboard,
&mut shell,
&Rectangle::new(iced::Point::ORIGIN, Size::new(500., 40.)),
);
shell.is_event_captured()
}
}
fn key(key: Key, modified_key: Key, code: Code) -> Event {
Event::Keyboard(keyboard::Event::KeyPressed {
key,
modified_key,
physical_key: Physical::Code(code),
location: keyboard::Location::Standard,
modifiers: Modifiers::empty(),
text: None,
repeat: false,
})
}
fn paste() -> Event {
key(
Key::Character("v".into()),
Key::Character("v".into()),
Code::KeyV,
)
}
#[test]
fn paste_refusal_matches_child_focus_enabled_and_stored_modifier_protocol() {
for focused in [false, true] {
for enabled in [false, true] {
for modifiers in [
Modifiers::empty(),
Modifiers::SHIFT,
Modifiers::COMMAND,
Modifiers::COMMAND | Modifiers::ALT,
Modifiers::COMMAND | Modifiers::SHIFT,
] {
let mut mounted = Mounted::new(focused, enabled, modifiers);
let before = mounted.state().cursor();
let expected = focused && enabled && modifiers.command() && !modifiers.alt();
let captured = mounted.event(paste());
assert_eq!(
mounted.messages,
if expected {
vec![Signal::Rejected]
} else {
vec![]
}
);
assert_eq!(mounted.clipboard.reads.get(), usize::from(expected));
assert_eq!(mounted.state().cursor(), before);
if expected {
assert!(captured);
}
}
}
}
}
#[test]
fn effective_escape_reset_follows_child_after_early_returns() {
// Raw Escape mapped to Home must not clear only the guard's Command state.
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
mounted.event(key(
Key::Named(Named::Escape),
Key::Named(Named::Home),
Code::Escape,
));
assert!(mounted.state().is_focused());
assert!(mounted.event(paste()));
assert_eq!(mounted.messages, vec![Signal::Rejected]);
// Effective Escape clears both states; a later focus operation does not
// invent a new ModifiersChanged event or revive the previous Command state.
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
mounted.event(key(
Key::Named(Named::Home),
Key::Named(Named::Escape),
Code::Home,
));
assert!(!mounted.state().is_focused());
mounted.tree.children[0]
.state
.downcast_mut::<InputState>()
.focus();
mounted.event(paste());
assert!(mounted.messages.is_empty());
assert_eq!(mounted.clipboard.reads.get(), 0);
// Printable text returns before effective Escape and leaves modifiers live.
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
let mut event = key(
Key::Named(Named::Home),
Key::Named(Named::Escape),
Code::Home,
);
if let Event::Keyboard(keyboard::Event::KeyPressed { text, .. }) = &mut event {
*text = Some("x".into());
}
mounted.event(event);
assert!(mounted.state().is_focused());
assert_eq!(mounted.messages, vec![Signal::Input("x".into())]);
mounted.messages.clear();
mounted.event(paste());
assert_eq!(mounted.messages, vec![Signal::Rejected]);
}
#[test]
fn held_paste_cache_contains_only_previously_admitted_text() {
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
mounted.clipboard.text = "water".into();
mounted.event(paste());
assert_eq!(mounted.messages, vec![Signal::Input("water".into())]);
mounted.messages.clear();
mounted.clipboard.text = "other".into();
mounted.event(paste());
assert_eq!(
mounted.messages,
vec![Signal::Input("waterwater".into())],
"Iced reuses its first admitted paste while the key is held"
);
mounted.messages.clear();
let before = mounted.state().cursor();
mounted.clipboard.text = "bad\tvalue".into();
mounted.event(paste());
assert_eq!(mounted.messages, vec![Signal::Rejected]);
assert_eq!(mounted.state().cursor(), before);
mounted.messages.clear();
mounted.event(Event::Keyboard(keyboard::Event::KeyReleased {
key: Key::Character("v".into()),
modified_key: Key::Character("v".into()),
physical_key: Physical::Code(Code::KeyV),
location: keyboard::Location::Standard,
modifiers: Modifiers::empty(),
}));
mounted.clipboard.text = "later".into();
mounted.event(paste());
assert_eq!(
mounted.messages,
vec![Signal::Input("waterwaterlater".into())]
);
}
@@ -0,0 +1,303 @@
//! Display-only reuse after a guarded alias rename. This is not model authority.
use iced::{advanced::text::Renderer, widget::text_editor};
use std::sync::Arc;
/// Replace exact, unchanged-line-count source in an existing editor.
///
/// The caller must first authorize/commit the model change and must reconstruct
/// from accepted source on false. Preflight failures do not mutate the editor;
/// a failed final readback can leave it changed. On changing success the cursor
/// is at 0:0 without selection; the caller restores its approved origin/reveal.
/// Direct Content actions deliberately bypass App draft edits and Undo history.
pub(super) fn replace<R: Renderer>(
editor: &mut text_editor::Content<R>,
before: &str,
after: &str,
) -> bool {
if editor.text() != before {
return false;
}
let Some(old_lines) = lines(before) else {
return false;
};
let Some(new_lines) = lines(after) else {
return false;
};
if old_lines.len() != new_lines.len() || old_lines.len() != editor.line_count() {
return false;
}
let mut patches = Vec::new();
for (line, ((old, old_ending), (new, new_ending))) in
old_lines.into_iter().zip(new_lines).enumerate()
{
let Some(actual) = editor.line(line) else {
return false;
};
// Cosmic also recognizes bare CR and LFCR. Comparing the actual lines
// catches its greedy LFCR interpretation of e.g. "x\n\r\ny" too.
if old_ending != new_ending || actual.text != old || actual.ending != old_ending {
return false;
}
if old == new {
continue;
}
let mut start = old
.bytes()
.zip(new.bytes())
.take_while(|(a, b)| a == b)
.count();
while !old.is_char_boundary(start) || !new.is_char_boundary(start) {
start -= 1;
}
let mut suffix = old[start..]
.bytes()
.rev()
.zip(new[start..].bytes().rev())
.take_while(|(a, b)| a == b)
.count();
while !old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix)
{
suffix -= 1;
}
// All positions and owned payloads are prepared before the first edit.
patches.push((
line,
start,
old.len() - suffix,
Arc::new(new[start..new.len() - suffix].to_owned()),
));
}
if patches.is_empty() {
return true;
}
let empty = Arc::new(String::new());
for (line, start, end, replacement) in patches.into_iter().rev() {
select(editor, line, start, end);
editor.perform(text_editor::Action::Edit(text_editor::Edit::Paste(
replacement,
)));
}
// Iced overwrites its earliest-change marker on each Edit. An explicit
// zero-width selection + empty Paste changes no bytes, but marks line zero.
// This also refreshes previously unhighlighted lines before the first patch.
// It resets only line zero's text cache; other unchanged lines retain theirs.
select(editor, 0, 0, 0);
editor.perform(text_editor::Action::Edit(text_editor::Edit::Paste(empty)));
editor.text() == after
}
fn select<R: Renderer>(
editor: &mut text_editor::Content<R>,
line: usize,
start: usize,
end: usize,
) {
editor.move_to(text_editor::Cursor {
position: text_editor::Position {
line,
column: start,
},
// move_to(None) does not clear a previous selection in this Iced version.
selection: Some(text_editor::Position { line, column: end }),
});
}
fn lines(source: &str) -> Option<Vec<(&str, text_editor::LineEnding)>> {
use text_editor::LineEnding;
let mut result = Vec::new();
let mut parts = source.split('\n').peekable();
while let Some(part) = parts.next() {
let (text, ending) = if parts.peek().is_none() {
(part, LineEnding::None)
} else if let Some(text) = part.strip_suffix('\r') {
(text, LineEnding::CrLf)
} else {
(part, LineEnding::Lf)
};
if text.contains('\r')
|| (text.is_empty()
&& ending == LineEnding::CrLf
&& result
.last()
.is_some_and(|(_, previous)| *previous == LineEnding::Lf))
{
return None;
}
result.push((text, ending));
}
Some(result)
}
#[cfg(all(test, debug_assertions))]
mod tests {
use super::*;
use crate::views::source_tests::FontLayout;
use iced::advanced::{layout, text::Highlighter, widget::Tree};
use iced::{Element, Font, Length, Size};
use std::{cell::RefCell, ops::Range};
#[test]
fn exact_text_matrix_and_reverse_preserve_endings() {
for (before, after) in [
("", ""),
("", "alias"),
("alias", ""),
("same", "same"),
("a\n", "alias\n"),
("a\r\n", "alias\r\n"),
("\r\na\r\n\r\n", "\r\nlong_alias\r\n\r\n"),
("\na\n\n", "\nlong_alias\n\n"),
("a\r\nb\nc", "long_a\r\nlong_b\nlong_c"),
(
"雨🙂 e\u{301} @a @a // a",
"雨🙂 e\u{301} @a_long @a_long // a",
),
("@abc", "@abcdef"),
("@abcdef", "@abc"),
("@abc_suffix", "@def_suffix"),
("@none", "@identifier"),
// Shared prefix/suffix bytes inside distinct UTF-8 codepoints must
// not be used as String split positions, even outside alias scope.
("雨xé", "雪xê"),
("é", "©"),
(
"unchanged\n@a\nunchanged\n@a\n",
"unchanged\n@long\nunchanged\n@long\n",
),
] {
let mut editor = text_editor::Content::<FontLayout>::with_text(before);
assert!(
replace(&mut editor, before, after),
"{before:?} -> {after:?}"
);
assert_eq!(editor.text(), after);
let expected = text_editor::Content::<FontLayout>::with_text(after);
assert_eq!(editor.line_count(), expected.line_count());
for (actual, expected) in editor.lines().zip(expected.lines()) {
assert_eq!(actual.text, expected.text);
assert_eq!(actual.ending, expected.ending);
}
if before != after {
assert_eq!(
editor.cursor().position,
text_editor::Position { line: 0, column: 0 }
);
assert_eq!(editor.cursor().selection, None);
}
assert!(replace(&mut editor, after, before));
assert_eq!(editor.text(), before);
}
}
#[test]
fn all_preflight_refusals_leave_text_cursor_and_selection_unchanged() {
for (actual, before, after) in [
("live", "stale", "changed"),
("a\nb", "a\nb", "long\nb\nc"),
("a\nb", "a\nb", "long\r\nb"),
// Refusal on a later line must not apply the earlier planned edit.
("a\nb\r\n", "a\nb\r\n", "long\nb\n"),
("a\rb", "a\rb", "long\rb"),
("a\n\rb", "a\n\rb", "long\n\rb"),
("a\n\r\nb", "a\n\r\nb", "long\n\r\nb"),
// The replacement must not introduce a greedy LFCR boundary either.
("a\nx\r\n", "a\nx\r\n", "a\n\r\n"),
("a", "a", "long\r"),
] {
let mut editor = text_editor::Content::<FontLayout>::with_text(actual);
select(&mut editor, 0, 0, 1);
let cursor = editor.cursor();
let selection = editor.selection();
assert!(
!replace(&mut editor, before, after),
"{before:?} -> {after:?}"
);
assert_eq!(editor.text(), actual);
assert_eq!(editor.cursor(), cursor);
assert_eq!(editor.selection(), selection);
}
}
#[test]
fn unchanged_content_preserves_selection_and_next_direct_edit_works() {
let mut editor = text_editor::Content::<FontLayout>::with_text("@a");
select(&mut editor, 0, 1, 2);
let cursor = editor.cursor();
assert!(replace(&mut editor, "@a", "@a"));
assert_eq!(editor.cursor(), cursor);
assert!(replace(&mut editor, "@a", "@long"));
editor.move_to(text_editor::Cursor {
position: text_editor::Position { line: 0, column: 5 },
selection: None,
});
editor.perform(text_editor::Action::Edit(text_editor::Edit::Insert('!')));
assert_eq!(editor.text(), "@long!");
}
thread_local! {
static CHANGES: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
}
struct Tracked(crate::highlight::ArcadiaHighlighter);
impl Highlighter for Tracked {
type Settings = Arc<str>;
type Highlight = oa_core::language::TokenClass;
type Iterator<'a> = std::vec::IntoIter<(Range<usize>, Self::Highlight)>;
fn new(settings: &Self::Settings) -> Self {
Self(crate::highlight::ArcadiaHighlighter::new(settings))
}
fn update(&mut self, settings: &Self::Settings) {
self.0.update(settings);
}
fn change_line(&mut self, line: usize) {
CHANGES.with(|changes| changes.borrow_mut().push(line));
self.0.change_line(line);
}
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
self.0.highlight_line(line)
}
fn current_line(&self) -> usize {
self.0.current_line()
}
}
fn view(
editor: &text_editor::Content<FontLayout>,
source: Arc<str>,
) -> Element<'_, (), iced::Theme, FontLayout> {
text_editor(editor)
.font(Font::MONOSPACE)
.size(14)
.height(Length::Fill)
.highlight_with::<Tracked>(source, crate::highlight::format)
.into()
}
#[test]
fn already_laid_out_editor_notifies_highlighter_from_zero_after_later_line_patch() {
let before = "// earlier\n// unmodified\n@a";
let after = "// earlier\n// unmodified\n@none";
let mut editor = text_editor::Content::<FontLayout>::with_text(before);
let limits = layout::Limits::new(Size::ZERO, Size::new(400., 60.));
let mut tree = {
let mut widget = view(&editor, Arc::from(before));
let mut tree = Tree::new(widget.as_widget());
let _ = widget
.as_widget_mut()
.layout(&mut tree, &FontLayout, &limits);
tree
};
CHANGES.with(|changes| changes.borrow_mut().clear());
assert!(replace(&mut editor, before, after));
{
let mut widget = view(&editor, Arc::from(after));
let _ = widget
.as_widget_mut()
.layout(&mut tree, &FontLayout, &limits);
}
// Font/metrics/bounds are already stable. This notification must come
// from the helper's empty edit, not a fresh renderer initialization.
CHANGES.with(|changes| assert_eq!(*changes.borrow(), [0]));
assert_eq!(editor.text(), after);
}
}
@@ -0,0 +1,448 @@
//! Combined App-boundary oracles: the workers are completed independently and
//! no returned Task is polled. Rendering measurements use the real reveal path.
use super::*;
use crate::diagram_usage::{self as usage, Surface, Target as UsageTarget, Usage};
use iced::{Size, Vector};
fn model_id(number: u8) -> Id {
Id::parse_str(&format!("00000000-0000-0000-0000-{number:012}")).unwrap()
}
fn navigation_fixture() -> App {
let mut source = bundle();
let entry = source.files.get_mut("project.arcadia").unwrap();
*entry = entry.replace(
"include \"uses.arcadia\";",
"include \"uses.arcadia\"; include \"views.arcadia\";",
);
source.files.insert(
"views.arcadia".into(),
concat!(
"// Keep @pump and the exact distant placement.\r\n",
"diagram overview \"Pump overview\" id \"00000000-0000-0000-0000-000000000008\" { layer Oa; }\r\n",
"node placed_pump id \"00000000-0000-0000-0000-000000000009\" { diagram @overview; element @pump; position { x 12000; y 9000; }; size { width 260; height 94; }; }\r\n",
).into(),
);
let mut app = fixture();
app.document = Document::from_bundle(source.clone(), LIMITS).unwrap();
assert_eq!(app.document.bundle(), &source);
assert!(
!validate(app.project())
.iter()
.any(|issue| issue.severity == validation::Severity::Error)
);
app.source_workspace = Workspace::new(app.document.bundle(), true);
app.selected = app.project().id;
app.refresh_draft();
app.source_workspace.active = "uses.arcadia".into();
app.show_source_file(false);
seek_pump(&mut app);
app.canvas_size = Some(Size::new(1200., 700.));
app.zoom = 0.8;
app.pan = Vector::new(31., 47.);
app
}
fn seek_pump(app: &mut App) {
let source = app.source_workspace.text();
let offset = source.rfind("pump;").unwrap();
app.source_editor
.move_to(cursor(source, offset, offset + 4));
}
fn source_completion(app: &App, request: nav::Request) -> nav::Completion {
let snapshot = Arc::new(
app.with_source_input(|input| query::capture(input, query::Limits::default()).unwrap()),
);
let results = snapshot
.query(query::Query {
stamp: request.stamp,
path: &request.path,
byte_offset: request.offset,
action: request.action,
})
.unwrap();
nav::Completion {
request,
result: Ok((snapshot, Arc::new(results))),
}
}
fn diagram_completion(app: &App) -> usage::Completion {
let panel = app.diagram_usages.as_ref().unwrap();
let process = app.behavior.topology.usage_snapshot().into_query_state();
usage::Completion {
token: panel.token,
context: panel.context,
result: Ok(usage::collect(
app.project(),
panel.context.subject,
&process,
&behavior::scenario::State::default(),
)),
}
}
fn finish_lookup(app: &mut App) -> (Id, Usage) {
let completion = diagram_completion(app);
send(app, Message::DiagramUsagesReady(completion));
let panel = app.diagram_usages.as_ref().unwrap();
assert!(!panel.loading);
assert!(app.diagram_lookup_in_flight.is_none());
let found = panel
.usages
.iter()
.find(|u| {
u.surface == Surface::Saved(model_id(8))
&& u.target == UsageTarget::Placement(model_id(9))
})
.expect("the exact placed pump is represented")
.clone();
assert_eq!(found.subject, model_id(2));
assert_eq!((found.bounds.x, found.bounds.y), (12000., 9000.));
(panel.token, found)
}
fn navigate(app: &mut App) -> Usage {
let message = app.diagram_usage_message(model_id(2));
send(app, message);
let (token, found) = finish_lookup(app);
send(
app,
Message::DiagramUsage(usage::Event::Choose(token, found.clone())),
);
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.active_diagram, Some(model_id(8)));
assert_eq!(app.selected, model_id(2));
assert_eq!(app.selected_node, Some(model_id(9)));
assert!(app.diagram_usages.is_none());
assert!(app.pending_diagram_reveal.is_some());
assert!(app.diagram_focus(found.surface).is_some());
found
}
#[derive(Debug, PartialEq)]
struct Navigation {
page: Page,
selected: Id,
placement: Option<Id>,
diagram: Option<Id>,
zoom: f32,
pan: Vector,
pending: Option<(usage::Context, Usage, u64)>,
focus: Option<(usage::Context, Usage, Id)>,
canvas_epoch: u64,
camera_epoch: u64,
source_interaction: Id,
source_reveal: Option<Id>,
}
fn navigation(app: &App) -> Navigation {
Navigation {
page: app.page,
selected: app.selected,
placement: app.selected_node,
diagram: app.active_diagram,
zoom: app.zoom,
pan: app.pan,
pending: app.pending_diagram_reveal.clone(),
focus: app
.diagram_navigation_focus
.as_ref()
.map(|f| (f.context, f.usage.clone(), f.selected)),
canvas_epoch: app.canvas_epoch,
camera_epoch: app.camera_epoch,
source_interaction: app.source_symbols.interaction,
source_reveal: app.source_reveal_ticket(),
}
}
fn first_measurement(app: &mut App, found: &Usage) {
let (context, _, epoch) = app.pending_diagram_reveal.as_ref().unwrap();
let (context, epoch) = (*context, *epoch);
let actual = Size::new(500., 300.);
send(app, Message::DiagramRevealViewport(context, epoch, actual));
assert!(app.pending_diagram_reveal.is_none());
assert_eq!(app.canvas_size, Some(actual));
assert!((found.bounds.center_x() * app.zoom + app.pan.x - 250.).abs() < 0.05);
assert!((found.bounds.center_y() * app.zoom + app.pan.y - 150.).abs() < 0.05);
assert_eq!(app.selected, model_id(2));
assert_eq!(app.selected_node, Some(model_id(9)));
assert!(app.diagram_focus(found.surface).is_some());
}
#[test]
fn rename_modal_rejects_queued_diagram_and_source_navigation_without_losing_approval() {
let mut app = navigation_fixture();
let original = evidence(&app);
let snapshot = app.document.snapshot();
let queued = app.diagram_usage_message(model_id(2));
begin(&mut app);
input(&mut app, "send_water");
let confirm = preview(&mut app);
let form = app.source_rename.form.as_ref().unwrap();
let approved = (
form.invocation,
form.generation,
form.approval.as_ref().unwrap().id,
);
send(&mut app, queued);
let source = app.source_symbol_message(nav::SymbolEvent::DiagramUsages);
send(&mut app, source);
let definition = app.source_symbol_message(nav::SymbolEvent::Query(query::Action::Definition));
send(&mut app, definition);
assert!(app.diagram_usages.is_none());
assert!(app.diagram_lookup_in_flight.is_none());
assert!(app.source_symbols.in_flight.is_none());
assert!(!app.source_navigation_available());
let form = app.source_rename.form.as_ref().unwrap();
assert_eq!(
(
form.invocation,
form.generation,
form.approval.as_ref().unwrap().id
),
approved
);
assert_eq!(form.replacement, "send_water");
assert_eq!(evidence(&app), original);
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
send(&mut app, Message::Escape);
send(&mut app, confirm);
assert!(!app.source_rename.open());
assert_eq!(evidence(&app), original);
}
#[test]
fn loading_and_ready_diagram_choosers_reject_a_previously_captured_rename_begin() {
for finish in [false, true] {
let mut app = navigation_fixture();
let before = evidence(&app);
let queued_rename = app.source_rename_message();
let open = app.diagram_usage_message(model_id(2));
send(&mut app, open);
if finish {
finish_lookup(&mut app);
}
let panel = app.diagram_usages.as_ref().unwrap();
let token = panel.token;
let loading = panel.loading;
assert!(app.rename_admission().is_err());
assert!(!app.source_navigation_available());
send(&mut app, queued_rename);
assert!(!app.source_rename.open());
assert!(app.source_rename.in_flight.is_none());
let panel = app.diagram_usages.as_ref().unwrap();
assert_eq!((panel.token, panel.loading), (token, loading));
assert_eq!(evidence(&app), before);
send(&mut app, Message::DiagramUsage(usage::Event::Close(token)));
let rename = app.source_rename_message();
send(&mut app, rename);
assert!(
app.source_rename.open(),
"the dismissed chooser no longer owns input"
);
}
}
#[test]
fn late_target_and_preview_results_release_only_their_slot_and_preserve_first_camera_measurement() {
for preview_pending in [false, true] {
let mut app = navigation_fixture();
let before = evidence(&app);
let completion = if preview_pending {
begin(&mut app);
input(&mut app, "send_water");
let job = start_preview(&mut app);
preview_completion(&app, job)
} else {
let rename = app.source_rename_message();
send(&mut app, rename);
target_completion(&app, current_job(&app))
};
let occupied = completion.job.id;
event(&mut app, Event::Close(completion.job.invocation));
assert_eq!(app.source_rename.in_flight, Some(occupied));
let found = navigate(&mut app);
let location = navigation(&app);
assert_eq!(evidence(&app), before);
let mut forged = completion.clone();
forged.job.id = Id::new_v4();
send(&mut app, Message::SourceRenameReady(forged));
assert_eq!(app.source_rename.in_flight, Some(occupied));
assert_eq!(navigation(&app), location);
send(&mut app, Message::SourceRenameReady(completion.clone()));
assert!(app.source_rename.in_flight.is_none());
assert!(!app.source_rename.open());
assert_eq!(navigation(&app), location);
assert_eq!(evidence(&app), before);
send(&mut app, Message::SourceRenameReady(completion));
assert_eq!(
navigation(&app),
location,
"duplicate completion is passive too"
);
first_measurement(&mut app, &found);
assert_eq!(evidence(&app), before);
}
}
#[test]
fn stale_begin_close_and_actual_confirm_leave_new_diagram_focus_and_pending_measurement_unchanged()
{
let mut app = navigation_fixture();
let before = evidence(&app);
let snapshot = app.document.snapshot();
let queued_begin = app.source_rename_message();
begin(&mut app);
input(&mut app, "send_water");
let confirm = preview(&mut app);
let invocation = app.source_rename.form.as_ref().unwrap().invocation;
event(&mut app, Event::Close(invocation));
let found = navigate(&mut app);
let location = navigation(&app);
for stale in [
queued_begin,
Message::SourceRename(Event::Close(invocation)),
confirm,
] {
send(&mut app, stale);
assert_eq!(navigation(&app), location);
assert_eq!(evidence(&app), before);
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
}
first_measurement(&mut app, &found);
}
#[test]
fn document_reset_retains_all_three_worker_slots_until_their_exact_results_arrive() {
let mut app = navigation_fixture();
let source = app.source_symbol_message(nav::SymbolEvent::Query(query::Action::Definition));
send(&mut app, source);
let request = app.source_symbols.request.clone().unwrap();
let source_ready = source_completion(&app, request.clone());
let rename = app.source_rename_message();
send(&mut app, rename);
let rename_ready = target_completion(&app, current_job(&app));
let rename_job = rename_ready.job.id;
event(&mut app, Event::Close(rename_ready.job.invocation));
let open = app.diagram_usage_message(model_id(2));
send(&mut app, open);
let old_diagram = diagram_completion(&app);
let diagram_job = old_diagram.token;
let old_document = app.canvas_document;
let project = app.project().clone();
app.reset(project, None);
assert_ne!(app.canvas_document, old_document);
assert_eq!(app.source_symbols.in_flight, Some(request.job));
assert_eq!(app.source_rename.in_flight, Some(rename_job));
assert_eq!(app.diagram_lookup_in_flight, Some(diagram_job));
assert!(!app.source_rename.open());
assert!(app.diagram_usages.is_none());
let before = evidence(&app);
let snapshot = app.document.snapshot();
let new_open = app.diagram_usage_message(model_id(2));
send(&mut app, new_open);
let new_token = app.diagram_usages.as_ref().unwrap().token;
assert_ne!(new_token, diagram_job);
assert_eq!(
app.diagram_lookup_in_flight,
Some(diagram_job),
"replacement waits for old worker"
);
send(&mut app, Message::SourceRenameReady(rename_ready));
assert!(app.source_rename.in_flight.is_none());
assert_eq!(app.source_symbols.in_flight, Some(request.job));
assert_eq!(app.diagram_lookup_in_flight, Some(diagram_job));
send(&mut app, Message::SourceSymbolsReady(source_ready));
assert!(app.source_symbols.in_flight.is_none());
assert!(!app.source_symbols.open);
send(&mut app, Message::DiagramUsagesReady(old_diagram));
assert_eq!(app.diagram_lookup_in_flight, Some(new_token));
assert!(app.diagram_usages.as_ref().unwrap().loading);
finish_lookup(&mut app);
assert_eq!(evidence(&app), before);
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert!(!app.source_rename.open());
}
#[test]
fn rename_then_exact_caret_diagram_navigation_preserves_complete_model_undo_redo_history() {
let mut app = navigation_fixture();
let baseline = app.document.snapshot();
let project = app.project().id;
app.document
.execute(Command::Rename(project, "Previously accepted name".into()))
.unwrap();
app.sync_source();
app.refresh_draft();
seek_pump(&mut app);
let before = app.document.snapshot();
let mut expected_project = app.project().clone();
expected_project
.elements
.get_mut(&model_id(2))
.unwrap()
.alias = "send_water".into();
let mut expected_source = app.document.bundle().clone();
let defs = expected_source.files.get_mut("defs.arcadia").unwrap();
*defs = defs.replace("oa_activity pump ", "oa_activity send_water ");
let uses = expected_source.files.get_mut("uses.arcadia").unwrap();
*uses = uses
.replace("source @pump;", "source @send_water;")
.replace("// keep @pump\r\n pump;", "// keep @pump\r\n send_water;");
let views = expected_source.files.get_mut("views.arcadia").unwrap();
*views = views.replace("element @pump;", "element @send_water;");
begin(&mut app);
input(&mut app, "send_water");
let confirm = preview(&mut app);
send(&mut app, confirm);
assert_eq!(app.project(), &expected_project);
assert_eq!(app.document.bundle(), &expected_source);
let renamed = app.document.snapshot();
let accepted = evidence(&app);
let queued_undo_rename = app
.source_rename_undo_message()
.expect("fresh rename offers its guarded undo");
let message = app.source_symbol_message(nav::SymbolEvent::DiagramUsages);
send(&mut app, message);
let request = app
.source_symbols
.request
.clone()
.expect("renamed exact caret remains queryable");
let ready = source_completion(&app, request);
send(&mut app, Message::SourceSymbolsReady(ready));
let (token, found) = finish_lookup(&mut app);
send(
&mut app,
Message::DiagramUsage(usage::Event::Choose(token, found.clone())),
);
assert_eq!(app.selected, model_id(2));
assert_eq!(app.selected_node, Some(model_id(9)));
let destination = navigation(&app);
send(&mut app, queued_undo_rename);
assert_eq!(navigation(&app), destination);
assert_eq!(evidence(&app), accepted);
first_measurement(&mut app, &found);
assert_eq!(evidence(&app), accepted);
assert!(Arc::ptr_eq(&renamed, &app.document.snapshot()));
for (message, expected) in [
(Message::Undo, before.clone()),
(Message::Redo, renamed.clone()),
(Message::Undo, before.clone()),
(Message::Undo, baseline.clone()),
(Message::Redo, before),
(Message::Redo, renamed),
] {
send(&mut app, message);
assert!(
Arc::ptr_eq(&expected, &app.document.snapshot()),
"exact history entry restored"
);
assert_eq!(app.project(), &expected.project);
assert_eq!(app.document.bundle(), &expected.bundle);
assert_eq!(app.source_workspace.bundle(), expected.bundle);
assert!(!app.source_dirty());
}
}
@@ -0,0 +1,449 @@
//! Real paragraph advances and clipping through the actual preview dialog.
//! Native screenshots additionally check raster ink and the platform renderer.
use super::*;
use crate::views::source_tests::FontLayout;
use iced::advanced::{Layout, layout, renderer, text as shaped, widget};
use iced::{Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector};
use std::collections::{BTreeMap, BTreeSet};
struct Glyph {
text: String,
paragraph_y: u32,
line: usize,
bytes: std::ops::Range<usize>,
bounds: Rectangle,
clip: Rectangle,
}
struct GlyphRecorder {
wanted: BTreeSet<String>,
transforms: Vec<Transformation>,
clips: Vec<Rectangle>,
glyphs: Vec<Glyph>,
quads: Vec<Rectangle>,
}
impl GlyphRecorder {
fn new(wanted: BTreeSet<String>, viewport: Rectangle) -> Self {
Self {
wanted,
transforms: vec![Transformation::IDENTITY],
clips: vec![viewport],
glyphs: vec![],
quads: vec![],
}
}
fn transform(&self) -> Transformation {
*self.transforms.last().unwrap()
}
fn clip(&self, bounds: Rectangle) -> Rectangle {
self.clips
.last()
.unwrap()
.intersection(&(bounds * self.transform()))
.unwrap_or(Rectangle::with_size(Size::ZERO))
}
}
impl renderer::Renderer for GlyphRecorder {
fn start_layer(&mut self, bounds: Rectangle) {
self.clips.push(self.clip(bounds));
}
fn end_layer(&mut self) {
self.clips.pop().unwrap();
}
fn start_transformation(&mut self, transform: Transformation) {
self.transforms.push(self.transform() * transform);
}
fn end_transformation(&mut self) {
self.transforms.pop().unwrap();
}
fn reset(&mut self, bounds: Rectangle) {
self.transforms = vec![Transformation::IDENTITY];
self.clips = vec![bounds];
self.glyphs.clear();
self.quads.clear();
}
fn fill_quad(&mut self, quad: renderer::Quad, _: impl Into<Background>) {
self.quads.push(quad.bounds * self.transform());
}
fn allocate_image(
&mut self,
handle: &iced::advanced::image::Handle,
callback: impl FnOnce(Result<iced::advanced::image::Allocation, iced::advanced::image::Error>)
+ Send
+ 'static,
) {
renderer::Renderer::allocate_image(&mut FontLayout, handle, callback);
}
}
impl shaped::Renderer for GlyphRecorder {
type Font = Font;
type Paragraph = <FontLayout as shaped::Renderer>::Paragraph;
type Editor = <FontLayout as shaped::Renderer>::Editor;
const ICON_FONT: Font = <FontLayout as shaped::Renderer>::ICON_FONT;
const CHECKMARK_ICON: char = <FontLayout as shaped::Renderer>::CHECKMARK_ICON;
const ARROW_DOWN_ICON: char = <FontLayout as shaped::Renderer>::ARROW_DOWN_ICON;
const SCROLL_UP_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_UP_ICON;
const SCROLL_DOWN_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_DOWN_ICON;
const SCROLL_LEFT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_LEFT_ICON;
const SCROLL_RIGHT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_RIGHT_ICON;
const ICED_LOGO: char = <FontLayout as shaped::Renderer>::ICED_LOGO;
fn default_font(&self) -> Font {
shaped::Renderer::default_font(&FontLayout)
}
fn default_size(&self) -> Pixels {
shaped::Renderer::default_size(&FontLayout)
}
fn fill_paragraph(
&mut self,
paragraph: &Self::Paragraph,
position: Point,
_: Color,
clip: Rectangle,
) {
let buffer = paragraph.buffer();
let text = buffer
.lines
.iter()
.map(|line| line.text())
.collect::<Vec<_>>()
.join("\n");
if !self.wanted.contains(&text) {
return;
}
let clip = self.clip(clip);
for run in buffer.layout_runs() {
for glyph in run.glyphs {
if run.text[glyph.start..glyph.end]
.chars()
.all(char::is_whitespace)
{
continue;
}
let bounds = Rectangle {
x: position.x + glyph.x + glyph.x_offset * glyph.font_size,
y: position.y + run.line_top,
width: glyph.w,
height: run.line_height,
} * self.transform();
self.glyphs.push(Glyph {
text: text.clone(),
paragraph_y: position.y.to_bits(),
line: run.line_i,
bytes: glyph.start..glyph.end,
bounds,
clip,
});
}
}
}
fn fill_editor(
&mut self,
editor: &Self::Editor,
position: Point,
color: Color,
clip: Rectangle,
) {
shaped::Renderer::fill_editor(&mut FontLayout, editor, position, color, clip);
}
fn fill_text(&mut self, text: shaped::Text, position: Point, color: Color, clip: Rectangle) {
shaped::Renderer::fill_text(&mut FontLayout, text, position, color, clip);
}
}
#[derive(Default)]
struct ScrollGeometry {
id: Option<widget::Id>,
bounds: Option<Rectangle>,
content: Option<Rectangle>,
offset: Vector,
}
impl widget::Operation for ScrollGeometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == self.id.as_ref() {
self.bounds = Some(bounds);
self.content = Some(content);
self.offset = offset;
}
}
}
fn normalized(text: &str) -> String {
text.replace("\r\n", "\n").replace('\r', "\n")
}
fn required_chars(text: &str) -> BTreeSet<(usize, usize)> {
text.split('\n')
.enumerate()
.flat_map(|(line, text)| {
text.char_indices()
.filter(|(_, ch)| !ch.is_whitespace())
.map(move |(byte, _)| (line, byte))
})
.collect()
}
fn declarations(long: bool) -> App {
let name = if long {
format!("Assess {} request", "界🚀".repeat(90))
} else {
"Assess 救援 request".into()
};
let source = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
("project.arcadia".into(), "arcadia 7;\r\nproject navigation_trial \"Navigation — 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n include \"救援/assess.arcadia\";\r\n}\r\n".into()),
("救援/assess.arcadia".into(), format!("// Comment @assess must not be a lexical use.\r\noa_activity assess \"{name}\" id \"9e8f0000-0000-4000-8000-000000000002\" {{\r\n description \"Keep @assess as prose, not a use.\";\r\n}}\r\n")),
]) };
let mut app = fixture();
app.document = Document::from_bundle(source, LIMITS).unwrap();
app.source_workspace = Workspace::new(app.document.bundle(), true);
app.source_workspace.active = "救援/assess.arcadia".into();
app.selected = app.project().id;
app.refresh_draft();
app.show_source_file(false);
let start = app
.source_workspace
.text()
.find("oa_activity assess")
.unwrap()
+ "oa_activity ".len();
app.source_editor.move_to(cursor(
app.source_workspace.text(),
start,
start + "assess".len(),
));
begin(&mut app);
let replacement = if long {
"a".repeat(255)
} else {
"assess_damage".into()
};
input(&mut app, &replacement);
let _ = preview(&mut app);
app
}
fn assert_page_glyphs_visible(app: &App, host: Size) {
let form = app.source_rename.form.as_ref().unwrap();
let ready = form.approval.as_ref().unwrap();
let file = &ready.preview.files()[form.file];
let mut expected = BTreeMap::<String, usize>::new();
for line in file.hunks()[form.hunk]
.lines()
.iter()
.skip(form.line)
.take(LINES_PER_PAGE)
{
for source in [
line.before_text(&ready.prepared.before().bundle.files[file.path()])
.unwrap(),
line.after_text(&ready.prepared.after().bundle.files[file.path()])
.unwrap(),
] {
if let Some(fragment) = preview::text_segment(source, form.segment) {
let text = normalized(fragment);
if !required_chars(&text).is_empty() {
*expected.entry(text).or_default() += 1;
}
}
}
}
assert!(!expected.is_empty());
let viewport = Rectangle::new(Point::ORIGIN, host);
let mut renderer = GlyphRecorder::new(expected.keys().cloned().collect(), viewport);
let editor = text_editor::Content::<GlyphRecorder>::new();
let mut view = dialog_content(app, &editor);
let mut tree = widget::Tree::new(view.as_widget());
let node =
view.as_widget_mut()
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, host));
let mut geometry = ScrollGeometry {
id: Some(preview_scroll_id(form, ready.id)),
..ScrollGeometry::default()
};
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &renderer, &mut geometry);
let bounds = geometry.bounds.unwrap();
let content = geometry.content.unwrap();
assert!(bounds.height >= 40.);
let maximum = (content.height - bounds.height).max(0.);
let steps = (maximum / (bounds.height * 0.5)).ceil() as usize;
let mut visible = BTreeMap::<(String, u32), BTreeSet<(usize, usize)>>::new();
let mut reserved_bar_gap = true;
for step in 0..=steps {
let offset = (step as f32 * bounds.height * 0.5).min(maximum);
view.as_widget_mut().operate(
&mut tree,
Layout::new(&node),
&renderer,
&mut widget::operation::scrollable::scroll_to(
preview_scroll_id(form, ready.id),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(offset),
},
),
);
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &renderer, &mut geometry);
assert!(
(geometry.offset.y - offset).abs() <= 0.5,
"actual scroll operation reaches the requested source offset"
);
renderer::Renderer::reset(&mut renderer, viewport);
view.as_widget().draw(
&tree,
&mut renderer,
&iced::Theme::Light,
&renderer::Style::default(),
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&viewport,
);
// The vertical scrollbar's actual painted thumb fixes its excluded x
// region; the outer scroll clip alone cannot detect overpainting.
let scrollbar_left = renderer
.quads
.iter()
.filter(|quad| {
quad.width > 0.
&& quad.width <= 16.
&& quad.height > 0.
&& quad.x >= bounds.x + bounds.width - 18.
&& quad.x + quad.width <= bounds.x + bounds.width + 0.5
&& quad.y >= bounds.y - 0.5
&& quad.y + quad.height <= bounds.y + bounds.height + 0.5
})
.map(|quad| quad.x)
.min_by(f32::total_cmp);
if maximum > 0. {
let left = scrollbar_left.expect("overflowing actual preview must paint its scrollbar");
reserved_bar_gap &= content.x + content.width <= left - 7.5;
}
for glyph in &renderer.glyphs {
let right = glyph.bounds.x + glyph.bounds.width;
if let Some(left) = scrollbar_left {
assert!(
right <= left - 0.5,
"source glyph under scrollbar: text={:?} cluster={:?} glyph={:?} bar x={left}",
glyph.text,
glyph.bytes,
glyph.bounds
);
}
assert!(
glyph.bounds.x >= content.x - 0.5 && right <= content.x + content.width + 0.5,
"source glyph escaped reserved width: {:?} {:?}",
glyph.text,
glyph.bounds
);
assert!(
glyph.bounds.x >= glyph.clip.x - 0.5
&& right <= glyph.clip.x + glyph.clip.width + 0.5,
"source glyph horizontally clipped: {:?} {:?} {:?}",
glyph.text,
glyph.bounds,
glyph.clip
);
if glyph.bounds.y >= glyph.clip.y - 0.5
&& glyph.bounds.y + glyph.bounds.height <= glyph.clip.y + glyph.clip.height + 0.5
{
let logical_line = glyph.text.split('\n').nth(glyph.line).unwrap();
let chars = logical_line
.char_indices()
.filter(|(byte, ch)| glyph.bytes.contains(byte) && !ch.is_whitespace())
.map(|(byte, _)| (glyph.line, byte));
visible
.entry((glyph.text.clone(), glyph.paragraph_y))
.or_default()
.extend(chars);
}
}
}
assert!(
reserved_bar_gap,
"source content must reserve actual scrollbar width plus gap"
);
assert!(
bounds.width >= host.width - 30.,
"preview must use available dialog width: {bounds:?} / {host:?}"
);
for (text, count) in expected {
let required = required_chars(&text);
let instances: Vec<_> = visible
.iter()
.filter(|((value, _), _)| value == &text)
.collect();
assert_eq!(
instances.len(),
count,
"all before/after occurrences must be seen: {text:?}"
);
for (_, seen) in instances {
assert_eq!(
seen, &required,
"every non-whitespace source cluster must be fully visible at some vertical offset: {text:?}"
);
}
}
}
#[test]
fn actual_preview_source_glyphs_clear_scrollbar_and_all_long_fragments_are_reachable() {
for long in [false, true] {
let mut app = declarations(long);
let ready = app
.source_rename
.form
.as_ref()
.unwrap()
.approval
.as_ref()
.unwrap()
.clone();
let invocation = app.source_rename.form.as_ref().unwrap().invocation;
let before = evidence(&app);
for (width, height) in [(1100., 700.), (580., 700.)] {
app.window_size = Size::new(width, height);
let host = Size::new((width - 84.).min(708.), height - 120.);
let total = segment_total(app.source_rename.form.as_ref().unwrap());
if long {
assert!(
total >= 3,
"include unbroken ASCII and wide Unicode across segments"
);
}
for segment in 0..total {
assert_page_glyphs_visible(&app, host);
if segment + 1 < total {
event(&mut app, Event::Segment(invocation, ready.id, true));
}
}
for _ in 1..total {
event(&mut app, Event::Segment(invocation, ready.id, false));
}
}
assert_eq!(
evidence(&app),
before,
"preview geometry/paging cannot alter transaction inputs"
);
assert_eq!(
app.source_rename
.form
.as_ref()
.unwrap()
.approval
.as_ref()
.unwrap()
.id,
ready.id
);
}
}
@@ -0,0 +1,385 @@
//! Execute the controller's scroll operations on a retained, scrolled dialog.
use super::*;
use crate::views::source_tests::FontLayout;
use iced::advanced::{Layout, layout, widget};
use iced::futures::StreamExt;
use iced::{Rectangle, Size, Vector};
const HOST: Size = Size::new(720., 660.);
fn paged_fixture() -> App {
let mut source = bundle();
let name = "Long engineering name 雨 ".repeat(18);
source.files.get_mut("defs.arcadia").unwrap().replace_range(
..,
&format!(
"oa_activity pump \"{name}\" id \"00000000-0000-0000-0000-000000000002\" {{}}\r\n\
oa_activity backup \"Backup\" id \"00000000-0000-0000-0000-000000000004\" {{}}\r\n"
),
);
let mut uses = String::new();
for index in 0..16 {
if index == 12 {
uses.push_str(&"// unchanged gap\r\n".repeat(10));
}
uses.push_str(&format!(
"interaction use_{index} \"{name}\" id \"00000000-0000-0000-0001-{index:012}\" {{ source @pump; target @backup; }}\r\n"
));
}
source.files.insert("uses.arcadia".into(), uses);
let mut app = fixture();
app.window_size = HOST;
app.document = Document::from_bundle(source, LIMITS).unwrap();
app.source_workspace = Workspace::new(app.document.bundle(), true);
app.source_workspace.active = "uses.arcadia".into();
app.show_source_file(false);
let start = app.source_workspace.text().find("source @pump").unwrap() + "source @".len();
app.source_editor
.move_to(cursor(app.source_workspace.text(), start, start + 4));
begin(&mut app);
input(&mut app, "pump_long");
let _ = preview(&mut app);
app
}
fn scroll_id(app: &App) -> widget::Id {
let form = app.source_rename.form.as_ref().unwrap();
preview_scroll_id(form, form.approval.as_ref().unwrap().id)
}
struct Geometry {
id: widget::Id,
offset: Option<Vector>,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
_: Rectangle,
_: Rectangle,
offset: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == Some(&self.id) {
self.offset = Some(offset);
}
}
}
struct Mounted {
editor: text_editor::Content<FontLayout>,
tree: widget::Tree,
}
impl Mounted {
fn new(app: &App) -> Self {
let editor = text_editor::Content::<FontLayout>::new();
let tree = widget::Tree::new(dialog_content(app, &editor).as_widget());
Self { editor, tree }
}
fn operate(&mut self, app: &App, operation: &mut dyn widget::Operation) {
let mut view = dialog_content(app, &self.editor);
// Preserve the actual scroll state across the new page's view diff.
self.tree.diff(view.as_widget());
let node = view.as_widget_mut().layout(
&mut self.tree,
&FontLayout,
&layout::Limits::new(Size::ZERO, HOST),
);
view.as_widget_mut()
.operate(&mut self.tree, Layout::new(&node), &FontLayout, operation);
}
fn offset(&mut self, app: &App) -> f32 {
let mut geometry = Geometry {
id: scroll_id(app),
offset: None,
};
self.operate(app, &mut geometry);
geometry.offset.expect("mounted preview scrollable").y
}
fn scroll_down(&mut self, app: &App) -> f32 {
self.operate(
app,
&mut widget::operation::scrollable::scroll_to(
scroll_id(app),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(10_000.),
},
),
);
let offset = self.offset(app);
assert!(offset > 10., "fixture must really be scrolled");
offset
}
async fn execute(&mut self, app: &App, task: Task<Message>) -> usize {
let mut count = 0;
if let Some(mut actions) = iced_runtime::task::into_stream(task) {
while let Some(action) = actions.next().await {
let iced_runtime::Action::Widget(mut operation) = action else {
panic!("preview paging must only schedule a widget operation");
};
self.operate(app, operation.as_mut());
count += 1;
}
}
count
}
}
#[tokio::test]
async fn explicit_navigation_resets_retained_scroll_but_id_change_alone_does_not() {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
mounted.scroll_down(&app);
let old_id = scroll_id(&app);
let task = app.update(Message::SourceRename(Event::File(
invocation, approval, true,
)));
assert_ne!(old_id, scroll_id(&app));
// Negative control: even after diff/layout with the new id and new file,
// Iced retains the previous offset until the returned operation executes.
assert!(mounted.offset(&app) > 10.);
assert_eq!(mounted.execute(&app, task).await, 1);
assert_eq!(mounted.offset(&app), 0.);
for navigation in [
Event::Line(invocation, approval, true),
Event::Segment(invocation, approval, true),
Event::Hunk(invocation, approval, true),
Event::File(invocation, approval, false),
Event::Search(invocation, "uses".into()),
Event::Search(invocation, "no-path-match".into()),
Event::Search(invocation, String::new()),
] {
mounted.scroll_down(&app);
let task = app.update(Message::SourceRename(navigation));
assert_eq!(mounted.execute(&app, task).await, 1);
assert_eq!(mounted.offset(&app), 0.);
assert_eq!(evidence(&app), before);
}
// Passive redraw and unchanged filter input preserve deliberate scrolling.
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.offset(&app), offset);
let task = app.update(Message::SourceRename(Event::Search(
invocation,
String::new(),
)));
assert_eq!(mounted.execute(&app, task).await, 0);
assert_eq!(mounted.offset(&app), offset);
let task = app.update(Message::SourceRename(Event::Input(
invocation,
"pump_long".into(),
)));
assert_eq!(mounted.execute(&app, task).await, 0);
assert_eq!(mounted.offset(&app), offset);
let task = app.update(Message::SourceRename(Event::File(
invocation, approval, false,
)));
assert_eq!(
mounted.execute(&app, task).await,
0,
"clamped first-page request is a no-op"
);
assert_eq!(mounted.offset(&app), offset);
assert_eq!(evidence(&app), before);
}
#[tokio::test]
async fn delayed_page_and_approval_operations_cannot_reset_a_new_destination() {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let old_approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
let old_page_task = app.update(Message::SourceRename(Event::File(
invocation,
old_approval,
true,
)));
let next_page_task = app.update(Message::SourceRename(Event::Line(
invocation,
old_approval,
true,
)));
assert_eq!(mounted.execute(&app, next_page_task).await, 1);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, old_page_task).await, 1);
assert_eq!(mounted.offset(&app), offset, "old page target is absent");
let old_approval_task = app.update(Message::SourceRename(Event::Search(
invocation,
"defs".into(),
)));
input(&mut app, "pump_new");
let _ = preview(&mut app); // Real new approval in the same invocation/page.
assert_ne!(
app.source_rename
.form
.as_ref()
.unwrap()
.approval
.as_ref()
.unwrap()
.id,
old_approval
);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, old_approval_task).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"old approval target is absent"
);
let task = app.update(Message::SourceRename(Event::File(
invocation,
old_approval,
true,
)));
assert_eq!(
mounted.execute(&app, task).await,
0,
"stale envelope is rejected before scheduling"
);
assert_eq!(mounted.offset(&app), offset);
assert_eq!(evidence(&app), before);
}
#[tokio::test]
async fn repeated_page_visits_filters_and_fast_preview_completion_have_distinct_destinations() {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
let first_visit = app.update(Message::SourceRename(Event::File(
invocation, approval, true,
)));
let next_page = app.update(Message::SourceRename(Event::Line(
invocation, approval, true,
)));
assert_eq!(mounted.execute(&app, next_page).await, 1);
let return_visit = app.update(Message::SourceRename(Event::Line(
invocation, approval, false,
)));
assert_eq!(mounted.execute(&app, return_visit).await, 1);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, first_visit).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"old visit to the identical page is stale"
);
let old_filter = app.update(Message::SourceRename(Event::Search(
invocation,
"uses".into(),
)));
let new_filter = app.update(Message::SourceRename(Event::Search(
invocation,
"us".into(),
)));
assert_eq!(mounted.execute(&app, new_filter).await, 1);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, old_filter).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"old filter on the same page is stale"
);
input(&mut app, "pump_new");
let job = start_preview(&mut app);
let completion = preview_completion(&app, job);
// No intermediate frame mounts the temporary no-approval view. The old
// Scrollable survives even though the new approval has a different id.
let ready = app.update(Message::SourceRenameReady(completion));
assert!(mounted.offset(&app) > 10.);
assert_eq!(mounted.execute(&app, ready).await, 1);
assert_eq!(mounted.offset(&app), 0.);
assert_eq!(evidence(&app), before);
}
#[tokio::test]
async fn details_round_trip_retires_held_reset_without_changing_page_or_repeated_visibility_scroll()
{
// Exercise both an actually mounted Details view and a fast round trip
// between frames, where Iced retains the same preview Scrollable state.
for mount_details in [true, false] {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
mounted.scroll_down(&app);
let held_reset = app.update(Message::SourceRename(Event::File(
invocation, approval, true,
)));
let old_preview = scroll_id(&app);
let form = app.source_rename.form.as_ref().unwrap();
let page = (form.file, form.hunk, form.line, form.segment);
let show_details = app.update(Message::SourceRename(Event::Details(invocation, true)));
assert_eq!(mounted.execute(&app, show_details).await, 0);
let details_visit = scroll_id(&app);
assert_ne!(details_visit, old_preview);
if mount_details {
let mut hidden = Geometry {
id: old_preview.clone(),
offset: None,
};
mounted.operate(&app, &mut hidden);
assert!(
hidden.offset.is_none(),
"Details actually replaced the preview"
);
}
let repeat_details = app.update(Message::SourceRename(Event::Details(invocation, true)));
assert_eq!(mounted.execute(&app, repeat_details).await, 0);
assert_eq!(
scroll_id(&app),
details_visit,
"unchanged visibility keeps its visit"
);
let show_preview = app.update(Message::SourceRename(Event::Details(invocation, false)));
assert_eq!(
mounted.execute(&app, show_preview).await,
0,
"returning does not force a scroll reset"
);
let returned_visit = scroll_id(&app);
assert_ne!(returned_visit, old_preview);
assert_ne!(returned_visit, details_visit);
let form = app.source_rename.form.as_ref().unwrap();
assert_eq!((form.file, form.hunk, form.line, form.segment), page);
assert_eq!(form.approval.as_ref().unwrap().id, approval);
let offset = mounted.scroll_down(&app);
let repeat_preview = app.update(Message::SourceRename(Event::Details(invocation, false)));
assert_eq!(mounted.execute(&app, repeat_preview).await, 0);
assert_eq!(scroll_id(&app), returned_visit);
assert_eq!(
mounted.offset(&app),
offset,
"repeated Preview preserves deliberate scroll"
);
assert_eq!(mounted.execute(&app, held_reset).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"the previous visit cannot reset the returned preview"
);
assert_eq!(evidence(&app), before);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,313 @@
//! Manual replacement + next-layout/highlight evaluator; no GPU or native paint.
//! Child of source_rename_timing_tests, preserving the separate real App test.
use super::*;
use crate::{highlight, views::source_tests::FontLayout};
use iced::advanced::widget::operation::Focusable;
use iced::advanced::{Layout, graphics, layout, renderer, text as shaped, widget::Tree};
use iced::{
Background, Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Transformation,
};
// The same real Paragraph and Editor as FontLayout. During draw, retain only a
// Weak and fixed geometry; inspect glyphs after timing, never in fill_editor.
#[derive(Default)]
struct ObservedFontLayout {
drawn: Option<(graphics::text::editor::Weak, Point, Rectangle)>,
}
impl renderer::Renderer for ObservedFontLayout {
fn start_layer(&mut self, _: Rectangle) {}
fn end_layer(&mut self) {}
fn start_transformation(&mut self, _: Transformation) {}
fn end_transformation(&mut self) {}
fn reset(&mut self, _: Rectangle) {
self.drawn = None;
}
fn fill_quad(&mut self, _: renderer::Quad, _: impl Into<Background>) {}
fn allocate_image(
&mut self,
handle: &iced::advanced::image::Handle,
callback: impl FnOnce(Result<iced::advanced::image::Allocation, iced::advanced::image::Error>)
+ Send
+ 'static,
) {
renderer::Renderer::allocate_image(&mut FontLayout, handle, callback);
}
}
impl shaped::Renderer for ObservedFontLayout {
type Font = Font;
type Paragraph = <FontLayout as shaped::Renderer>::Paragraph;
type Editor = <FontLayout as shaped::Renderer>::Editor;
const ICON_FONT: Font = <FontLayout as shaped::Renderer>::ICON_FONT;
const CHECKMARK_ICON: char = <FontLayout as shaped::Renderer>::CHECKMARK_ICON;
const ARROW_DOWN_ICON: char = <FontLayout as shaped::Renderer>::ARROW_DOWN_ICON;
const SCROLL_UP_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_UP_ICON;
const SCROLL_DOWN_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_DOWN_ICON;
const SCROLL_LEFT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_LEFT_ICON;
const SCROLL_RIGHT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_RIGHT_ICON;
const ICED_LOGO: char = <FontLayout as shaped::Renderer>::ICED_LOGO;
fn default_font(&self) -> Font {
shaped::Renderer::default_font(&FontLayout)
}
fn default_size(&self) -> Pixels {
shaped::Renderer::default_size(&FontLayout)
}
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
fn fill_editor(&mut self, editor: &Self::Editor, position: Point, _: Color, clip: Rectangle) {
self.drawn = Some((editor.downgrade(), position, clip));
}
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
}
type Content = text_editor::Content<ObservedFontLayout>;
const HOST: Size = Size::new(640., 350.);
fn view(editor: &Content, source: Arc<str>) -> Element<'_, (), iced::Theme, ObservedFontLayout> {
text_editor(editor)
.on_action(|_| ())
.font(Font::MONOSPACE)
.size(14)
.padding(16)
.height(Length::Fill)
.highlight_with::<highlight::ArcadiaHighlighter>(source, highlight::format)
.into()
}
fn check_text(editor: &Content, expected: &str, cursor: text_editor::Cursor) {
assert_eq!(editor.text(), expected);
assert_eq!(editor.cursor(), cursor);
let lines: Vec<_> = expected.split('\n').collect();
assert_eq!(editor.line_count(), lines.len());
for (index, part) in lines.iter().enumerate() {
let (text, ending) = if index + 1 == lines.len() {
(*part, text_editor::LineEnding::None)
} else if let Some(text) = part.strip_suffix('\r') {
(text, text_editor::LineEnding::CrLf)
} else {
(*part, text_editor::LineEnding::Lf)
};
let actual = editor.line(index).unwrap();
assert_eq!(actual.text, text);
assert_eq!(actual.ending, ending);
}
}
fn check_draw(renderer: &ObservedFontLayout, cursor: text_editor::Cursor, alias: &str) {
let (weak, position, clip) = renderer.drawn.as_ref().expect("real editor draw");
let editor = weak.upgrade().expect("drawn editor is still live");
let buffer = editor.buffer();
assert_eq!(weak.bounds, Size::new(608., 318.));
assert_eq!(buffer.size(), (Some(608.), Some(318.)));
assert_eq!(buffer.metrics().font_size, 14.);
assert_eq!(
buffer.scroll().horizontal,
0.,
"fixture origin fits the viewport"
);
let start = cursor.position;
let end = cursor.selection.expect("approved alias selection");
assert_eq!(start.line, end.line);
assert_eq!(end.column - start.column, alias.len());
let line = &buffer.lines[start.line];
assert_eq!(&line.text()[start.column..end.column], alias);
let color = graphics::text::to_color(Color::from_rgb8(20, 93, 139));
let font = graphics::text::to_attributes(Font::MONOSPACE);
for byte in start.column..end.column {
let attrs = line.attrs_list().get_span(byte);
assert_eq!(
attrs.color_opt,
Some(color),
"accepted alias syntax attributes"
);
assert_eq!(attrs.family, font.family);
}
let mut covered = vec![false; alias.len()];
for run in buffer.layout_runs().filter(|run| run.line_i == start.line) {
for glyph in run.glyphs {
let from = glyph.start.max(start.column);
let to = glyph.end.min(end.column);
if from >= to {
continue;
}
assert_eq!(glyph.color_opt, Some(color), "drawn alias glyph color");
let x = position.x + glyph.x + glyph.x_offset * glyph.font_size;
let y = position.y + run.line_top;
assert!(x >= clip.x - 0.5 && x + glyph.w <= clip.x + clip.width + 0.5);
assert!(y + run.line_height > clip.y && y < clip.y + clip.height);
covered[from - start.column..to - start.column].fill(true);
}
}
assert!(
covered.into_iter().all(|seen| seen),
"entire alias has visible shaped glyphs"
);
// This temporary strong upgrade is dropped before the next editor mutation.
}
#[derive(Clone, Copy)]
enum Arm {
Constructor,
Reuse,
}
impl Arm {
fn name(self) -> &'static str {
match self {
Self::Constructor => "with_text",
Self::Reuse => "same_line_reuse",
}
}
fn phases(self) -> [&'static str; 4] {
match self {
Self::Constructor => [
"constructor_replace_origin",
"constructor_next_layout",
"constructor_highlight_draw",
"constructor_combined",
],
Self::Reuse => [
"reuse_replace_origin",
"reuse_next_layout",
"reuse_highlight_draw",
"reuse_combined",
],
}
}
}
fn trial(
fixture: &Fixture,
cursor: text_editor::Cursor,
arm: Arm,
trial: usize,
samples: &mut Samples,
) {
let before = &fixture.bundle.files[ORIGIN];
let after = &fixture.expected_bundle.files[ORIGIN];
let mut editor = Content::with_text(before);
editor.move_to(fixture.cursor);
let mut renderer = ObservedFontLayout::default();
let theme = iced::Theme::Light;
let defaults = renderer::Style::default();
let viewport = Rectangle::with_size(HOST);
let limits = layout::Limits::new(Size::ZERO, HOST);
let mut tree = {
let mut widget = view(&editor, Arc::from(before.as_str()));
let mut tree = Tree::new(widget.as_widget());
let node = widget.as_widget_mut().layout(&mut tree, &renderer, &limits);
tree.state
.downcast_mut::<text_editor::State<highlight::ArcadiaHighlighter>>()
.focus();
widget.as_widget().draw(
&tree,
&mut renderer,
&theme,
&defaults,
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&viewport,
);
tree
};
check_text(&editor, before, fixture.cursor);
check_draw(&renderer, fixture.cursor, OLD);
// No recorder Weak/Strong from setup may perturb replacement ownership.
renderer.drawn = None;
let next_source: Arc<str> = Arc::from(after.as_str());
// Only normal operations and four timestamps occur between start and end.
// Source/model oracles, logging and recorder glyph inspection are outside.
let started = Instant::now();
let replaced = match arm {
Arm::Constructor => {
editor = Content::with_text(after);
true
}
Arm::Reuse => super::super::editor_patch::replace(&mut editor, before, after),
};
editor.move_to(cursor);
let replaced_at = Instant::now();
let mut widget = view(&editor, next_source);
let node = widget.as_widget_mut().layout(&mut tree, &renderer, &limits);
let laid_out_at = Instant::now();
widget.as_widget().draw(
&tree,
&mut renderer,
&theme,
&defaults,
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&viewport,
);
let drawn_at = Instant::now();
drop(widget);
assert!(replaced, "reuse must not silently time a fallback/refusal");
check_text(&editor, after, cursor);
check_draw(&renderer, cursor, NEW);
renderer.drawn = None;
for (phase, duration) in arm.phases().into_iter().zip([
replaced_at.duration_since(started),
laid_out_at.duration_since(replaced_at),
drawn_at.duration_since(laid_out_at),
drawn_at.duration_since(started),
]) {
println!(
"{}",
serde_json::json!({
"record": "editor_layout_timing", "arm": arm.name(), "phase": phase,
"trial": trial, "warmup": trial == 0,
"elapsed_ns": duration.as_nanos().to_string(),
"elapsed_ms": duration.as_secs_f64() * 1000.0,
})
);
if trial != 0 {
samples.0.entry(phase).or_default().push(duration);
}
}
}
#[test]
#[ignore = "manual Windows dev/debug editor replacement + layout/highlight trial; run alone with --ignored --nocapture --test-threads=1"]
fn accepted_release1_constructor_vs_reuse_through_next_highlight() {
let fixture = Fixture::load();
// Resolve the exact production approval and verify full Project/source once,
// outside all intervals. The actual App Confirm evaluator remains separate.
let cursor = {
let app = fixture.app();
let target = resolve(&app, &app.rename_context());
let prepared = app
.document
.alias_rename_request(target.id, NEW.into())
.unwrap()
.prepare()
.unwrap();
fixture.check_candidate(&prepared);
prepare_approval(prepared, &target, ORIGIN).unwrap().cursor
};
println!(
"{}",
serde_json::json!({
"record": "editor_layout_fixture", "host_width": HOST.width, "host_height": HOST.height,
"font": "MONOSPACE", "font_size": 14, "padding": 16, "height": "Fill",
"active_file": ORIGIN, "before_bytes": fixture.bundle.files[ORIGIN].len(),
"after_bytes": fixture.expected_bundle.files[ORIGIN].len(), "warmups_per_arm": 1,
"measured_trials_per_arm": 5, "debug_assertions": cfg!(debug_assertions),
"scope": "real editor replacement, retained Tree layout and Widget draw/highlight; no GPU, native paint or App Confirm",
})
);
let mut samples = Samples::default();
for index in 0..=5 {
let arms = if index % 2 == 0 {
[Arm::Constructor, Arm::Reuse]
} else {
[Arm::Reuse, Arm::Constructor]
};
println!(
"{}",
serde_json::json!({"record": "editor_layout_order", "trial": index, "first": arms[0].name()})
);
for arm in arms {
trial(&fixture, cursor, arm, index, &mut samples);
}
}
samples.report();
}
@@ -0,0 +1,347 @@
//! Manual timing through the real Document and App paths. No disk writes,
//! executor polling, layout, GPU work or first-paint measurement occurs here.
//! Register as a test child of app::source_rename to use its actual worker helpers.
use super::*;
use crate::source_workspace::Workspace;
use oa_core::source_project::SourceBundle;
use std::{
collections::BTreeMap,
path::PathBuf,
time::{Duration, Instant},
};
#[cfg(debug_assertions)]
#[path = "source_rename_layout_timing_tests.rs"]
mod layout_timing_tests;
const OLD: &str = "oa_restore_work";
const NEW: &str = "oa_restore_pressure";
const DECLARATIONS: &str = "oa/activities/activity_oa_restore_work.arcadia";
const ORIGIN: &str = "traceability.arcadia";
struct Fixture {
bundle: SourceBundle,
project: Project,
expected_bundle: SourceBundle,
expected_project: Project,
target: Id,
cursor: text_editor::Cursor,
}
impl Fixture {
fn load() -> Self {
// The optional entry selects a parent-pinned copy of this exact fixture,
// not an arbitrary performance generator or a mutable GUI destination.
let entry = std::env::var_os("ARCADIA_RENAME_TIMING_ENTRY")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/release1/project.arcadia")
});
let loaded = oa_core::source_files::load_project(&entry, LIMITS).unwrap();
let bundle = loaded.reachable_bundle();
assert_eq!(
bundle.files.len(),
30,
"use the preserved release1 walkthrough"
);
let document = Document::from_bundle(bundle.clone(), LIMITS).unwrap();
assert_eq!(
document.bundle(),
&bundle,
"fixture must need no stabilization/migration"
);
assert!(!document.dirty());
let project = document.project().clone();
let target = Id::parse_str("1b91f8a7-0bdb-514e-b1cd-955658e09927").unwrap();
assert_eq!(project.elements[&target].alias, OLD);
assert!(!project.identities().iter().any(|(_, alias)| *alias == NEW));
let mut expected_project = project.clone();
expected_project.elements.get_mut(&target).unwrap().alias = NEW.into();
// Independent literal source oracle, authored before running the rename.
// Do not replace the alias substring in filenames or the trace's alias.
let mut expected_bundle = bundle.clone();
let declarations = expected_bundle.files.get_mut(DECLARATIONS).unwrap();
assert_eq!(
declarations.matches("oa_activity oa_restore_work ").count(),
1
);
assert_eq!(declarations.matches("parent @oa_restore_work;").count(), 4);
*declarations = declarations
.replace(
"oa_activity oa_restore_work ",
"oa_activity oa_restore_pressure ",
)
.replace("parent @oa_restore_work;", "parent @oa_restore_pressure;");
let traces = expected_bundle.files.get_mut(ORIGIN).unwrap();
assert_eq!(traces.matches("oa @oa_restore_work;").count(), 1);
*traces = traces.replace("oa @oa_restore_work;", "oa @oa_restore_pressure;");
let source = &bundle.files[ORIGIN];
let start = source.rfind("@oa_restore_work;").unwrap() + 1;
let position = |offset| {
let prefix = &source[..offset];
text_editor::Position {
line: prefix.bytes().filter(|byte| *byte == b'\n').count(),
column: prefix
.rsplit_once('\n')
.map_or(prefix.len(), |(_, tail)| tail.len()),
}
};
let cursor = text_editor::Cursor {
position: position(start),
selection: Some(position(start + OLD.len())),
};
assert!(nav::exact_cursor(source, cursor));
println!(
"{}",
serde_json::json!({
"record": "fixture", "entry": entry, "reachable_files": bundle.files.len(),
"source_bytes": bundle.files.values().map(String::len).sum::<usize>(),
"identities": project.identities().len(), "active_file": ORIGIN,
"active_file_bytes": source.len(), "old": OLD, "new": NEW,
"warmups": 1, "measured_trials": 5, "debug_assertions": cfg!(debug_assertions),
"scope": "real worker helpers, guarded Document commit, App sync and Confirm update; no task polling or rendering"
})
);
Self {
bundle,
project,
expected_bundle,
expected_project,
target,
cursor,
}
}
fn app(&self) -> App {
let mut app = App::with_project(Project::empty());
app.document = Document::from_bundle(self.bundle.clone(), LIMITS).unwrap();
app.source_workspace = Workspace::new(app.document.bundle(), true);
app.source_workspace.active = ORIGIN.into();
app.selected = app.project().id;
app.page = Page::Text;
app.sync();
app.source_editor.move_to(self.cursor);
assert_eq!(app.project(), &self.project);
assert_eq!(app.document.bundle(), &self.bundle);
assert_eq!(app.document.revision(), 0);
assert!(!app.document.dirty() && !app.document.can_undo() && !app.document.can_redo());
assert!(app.rename_admission().is_ok());
app
}
fn check_candidate(&self, prepared: &PreparedAliasRename) {
assert!(prepared.changed());
assert_eq!(prepared.before().bundle, self.bundle);
assert_eq!(prepared.before().decoded.project, self.project);
assert_eq!(prepared.after().bundle, self.expected_bundle);
assert_eq!(prepared.after().decoded.project, self.expected_project);
}
fn check_adopted(&self, app: &mut App, candidate_address: *const CompiledSourceProject) {
assert!(
std::ptr::eq(app.document.compiled(), candidate_address),
"adopt exact candidate allocation"
);
assert_eq!(app.project(), &self.expected_project);
assert_eq!(app.document.bundle(), &self.expected_bundle);
assert_eq!(app.source_workspace.bundle(), self.expected_bundle);
assert_eq!(app.source_editor.text(), self.expected_bundle.files[ORIGIN]);
assert_eq!(app.document.revision(), 1);
assert!(app.document.dirty() && app.document.can_undo() && !app.document.can_redo());
assert!(app.document.undo());
assert_eq!(app.project(), &self.project);
assert_eq!(app.document.bundle(), &self.bundle);
assert!(
!app.document.dirty(),
"live known saved baseline remains authoritative"
);
assert!(
!app.document.can_undo(),
"exactly one preceding model transaction"
);
assert!(app.document.redo());
assert_eq!(app.project(), &self.expected_project);
assert_eq!(app.document.bundle(), &self.expected_bundle);
assert!(std::ptr::eq(app.document.compiled(), candidate_address));
}
}
#[derive(Default)]
struct Samples(BTreeMap<&'static str, Vec<Duration>>);
impl Samples {
fn time<T>(&mut self, phase: &'static str, trial: usize, action: impl FnOnce() -> T) -> T {
let started = Instant::now();
let result = action();
let elapsed = started.elapsed();
// Printing, assertions and dropping the returned value are outside this
// interval. Full Confirm still includes production's own local drops.
println!(
"{}",
serde_json::json!({
"record": "timing", "phase": phase, "trial": trial,
"warmup": trial == 0, "elapsed_ns": elapsed.as_nanos().to_string(),
"elapsed_ms": elapsed.as_secs_f64() * 1000.0
})
);
if trial != 0 {
self.0.entry(phase).or_default().push(elapsed);
}
result
}
fn report(mut self) {
for (phase, values) in &mut self.0 {
assert_eq!(values.len(), 5);
values.sort();
println!(
"{}",
serde_json::json!({
"record": "summary", "phase": phase, "samples": values.len(),
"min_ms": values[0].as_secs_f64() * 1000.0,
"median_ms": values[2].as_secs_f64() * 1000.0,
"max_ms": values[4].as_secs_f64() * 1000.0
})
);
}
}
}
fn resolve(app: &App, context: &Context) -> Target {
let snapshot = app
.with_source_input(|input| query::admit_owned(input, query::Limits::default()))
.unwrap()
.capture()
.unwrap();
let offset = nav::cursor_offset(app.source_workspace.text(), context.begin.cursor).unwrap();
resolve_target(&snapshot, context, offset).unwrap()
}
fn active_job(app: &App) -> Job {
let form = app.source_rename.form.as_ref().unwrap();
let id = form.pending.unwrap();
assert_eq!(app.source_rename.in_flight, Some(id));
Job {
id,
invocation: form.invocation,
generation: form.generation,
context: form.context.clone(),
}
}
fn separate_phases(fixture: &Fixture, samples: &mut Samples, trial: usize) {
let mut app = fixture.app();
let target = resolve(&app, &app.rename_context());
assert_eq!(target.id, fixture.target);
let request = samples
.time("document_request_mint", trial, || {
app.document.alias_rename_request(target.id, NEW.into())
})
.unwrap();
let prepared = samples
.time("worker_document_prepare", trial, || request.prepare())
.unwrap();
fixture.check_candidate(&prepared);
let approval = samples
.time("worker_preview_and_origin", trial, || {
prepare_approval(prepared, &target, ORIGIN)
})
.unwrap();
assert_eq!(approval.preview.files().len(), 2);
assert_eq!(approval.preview.changed_line_count(), 6);
let candidate_address = approval.prepared.after() as *const CompiledSourceProject;
// Same-destination save completion is a legitimate live update after Preview.
app.document.mark_saved(app.document.snapshot());
let changed = samples
.time("document_guarded_commit", trial, || {
app.document.commit_alias_rename(&approval.prepared)
})
.unwrap();
assert!(changed);
samples.time("app_sync", trial, || app.sync());
fixture.check_adopted(&mut app, candidate_address);
}
fn whole_confirm(fixture: &Fixture, samples: &mut Samples, trial: usize) {
let mut app = fixture.app();
let begin = app.source_rename_message();
// These Tasks are deliberately not polled: deterministic worker helpers
// supply the same results through the real readiness messages below.
drop(app.update(begin));
let target_job = active_job(&app);
let target = Arc::new(resolve(&app, &target_job.context));
assert_eq!(target.id, fixture.target);
drop(app.update(Message::SourceRenameReady(Completion {
job: target_job,
payload: Payload::Target(Ok(target.clone())),
})));
let invocation = app.source_rename.form.as_ref().unwrap().invocation;
drop(app.update(Message::SourceRename(Event::Input(invocation, NEW.into()))));
let generation = app.source_rename.form.as_ref().unwrap().generation;
drop(app.update(Message::SourceRename(Event::Preview(
invocation, generation,
))));
let preview_job = active_job(&app);
let prepared = app
.document
.alias_rename_request(target.id, NEW.into())
.unwrap()
.prepare()
.unwrap();
fixture.check_candidate(&prepared);
let approval = Arc::new(prepare_approval(prepared, &target, ORIGIN).unwrap());
drop(target);
let candidate_address = approval.prepared.after() as *const CompiledSourceProject;
let expected_cursor = approval.cursor;
let approval_id = approval.id;
drop(app.update(Message::SourceRenameReady(Completion {
job: preview_job,
payload: Payload::Preview(Ok(approval)),
})));
assert_eq!(
app.source_rename
.form
.as_ref()
.unwrap()
.approval
.as_ref()
.unwrap()
.id,
approval_id
);
app.document.mark_saved(app.document.snapshot());
let confirm = Message::SourceRename(Event::Confirm(invocation, generation, approval_id));
let task = samples.time("app_confirm_update", trial, || app.update(confirm));
samples.time("confirm_returned_task_drop_without_polling", trial, || {
drop(task)
});
assert!(!app.source_rename.open());
assert!(app.source_rename.in_flight.is_none());
assert!(app.source_rename_undo_message().is_some());
assert_eq!(app.source_editor.cursor(), expected_cursor);
fixture.check_adopted(&mut app, candidate_address);
}
#[test]
#[ignore = "manual Windows dev/debug rename latency trial; run alone with --ignored --nocapture --test-threads=1"]
fn accepted_release1_prepare_commit_sync_and_confirm_timing() {
let fixture = Fixture::load();
let mut samples = Samples::default();
for trial in 0..=5 {
println!(
"{}",
serde_json::json!({ "record": "order", "trial": trial,
"warmup": trial == 0, "separate_phases_first": trial % 2 == 0 })
);
if trial % 2 == 0 {
separate_phases(&fixture, &mut samples, trial);
whole_confirm(&fixture, &mut samples, trial);
} else {
whole_confirm(&fixture, &mut samples, trial);
separate_phases(&fixture, &mut samples, trial);
}
}
samples.report();
}
+3 -2
View File
@@ -52,7 +52,7 @@ impl App {
}
}
fn with_source_input<T>(&self, use_input: impl FnOnce(query::Input<'_>) -> T) -> T {
pub(super) fn with_source_input<T>(&self, use_input: impl FnOnce(query::Input<'_>) -> T) -> T {
let files: Vec<_> = self
.source_workspace
.files
@@ -83,13 +83,14 @@ impl App {
})
}
fn source_snapshot_matches(&self, snapshot: &query::Snapshot) -> bool {
pub(super) fn source_snapshot_matches(&self, snapshot: &query::Snapshot) -> bool {
snapshot.key() == self.source_key()
&& self.with_source_input(|input| snapshot.matches(input))
}
pub fn source_navigation_available(&self) -> bool {
self.page == Page::Text
&& !self.source_rename.open()
&& !self.busy
&& self.pending.is_none()
&& self.file_prompt.is_none()
@@ -0,0 +1,430 @@
//! Unwired single-line scenario label experiment. No renderer or schema is changed.
//!
//! Iced 0.14.0 WGPU `geometry::Frame::fill_text` shapes positive uniform-scale
//! canvas text at `size * zoom`; its `text::prepare` uses graphics text::Cache.
//! This adapter uses that same allocator/font system/attributes, not a character
//! width estimate. Canvas Text::max_width is wrapping, so it stays infinite.
//! Experimental parity requires settled fonts and a fresh renderer/cache after
//! font loading. Iced's WGPU text cache key omits FontSystem::Version, so merely
//! remeasuring here cannot repair its cached glyph buffers after dynamic loading.
//! A RenderSession is caller attestation of that lifecycle, not its enforcement.
//! No fonts may be loaded between measurement, guarded access and actual draw.
//!
//! Integration seam: after lifecycle/activation endpoint alignment, a caller
//! supplies a usable text width in world units and retains one cache per live
//! labelled occurrence across Scene rebuilds. Paint, hit testing, navigation and extents
//! consume the returned Label. Position/color do not invalidate measurement.
//! This does NOT choose a self-call, reversed-arrow or obstacle corridor policy.
//! It measures layout advances, not raster ink; overhang/clip/activation-border
//! acceptance still requires the real WGPU evaluator. The old 32-grapheme/225px
//! renderer is deliberately untouched for a later baseline comparison.
use iced::advanced::{
graphics::text as graphics_text,
text::{Alignment, LineHeight, Shaping},
};
use iced::{Color, Font, Pixels, Point, Rectangle, Size, widget::canvas::Text};
use std::{collections::VecDeque, ops::Range, sync::Arc};
use unicode_segmentation::UnicodeSegmentation;
const MAX_SOURCE_BYTES: usize = 64 * 1024;
const MAX_GRAPHEMES: usize = 4096;
const MAX_ENTRIES: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Style {
pub font: Font,
pub size: Pixels,
pub line_height: LineHeight,
pub shaping: Shaping,
}
impl Default for Style {
fn default() -> Self {
let text = Text::default();
Self {
font: text.font,
size: Pixels(12.),
line_height: text.line_height,
shaping: text.shaping,
}
}
}
impl Style {
fn scaled(self, zoom: f32) -> (f32, f32) {
let size = Pixels(self.size.0 * zoom);
let line_height = match self.line_height {
LineHeight::Absolute(value) => LineHeight::Absolute(Pixels(value.0 * zoom)),
LineHeight::Relative(value) => LineHeight::Relative(value),
};
(size.0, line_height.to_absolute(size).0)
}
}
/// Explicit experimental contract: the caller finishes loading fonts and starts
/// a fresh renderer/cache BEFORE capturing this session, then keeps fonts fixed.
/// This module cannot inspect or reset the actual renderer's text cache.
#[derive(Clone, Debug)]
pub(super) struct RenderSession {
version: graphics_text::Version,
identity: Arc<()>,
}
impl RenderSession {
pub fn after_fonts_settled_and_renderer_reset() -> Result<Self, Error> {
let fonts = graphics_text::font_system()
.read()
.map_err(|_| Error::FontSystemUnavailable)?;
Ok(Self {
version: fonts.version(),
identity: Arc::new(()),
})
}
}
#[derive(Clone, Debug)]
pub(super) struct Label {
source: String,
displayed: String,
/// Original-label bytes represented before any added ellipsis. Includes
/// collapsed whitespace, but `retains_any` never counts whitespace as text.
retained_source: Range<usize>,
/// World-space layout advances, at origin; not glyph-ink containment.
bounds: Rectangle,
style: Style,
measured_session: RenderSession,
measured_zoom: f32,
}
impl Label {
fn check(&self, session: &RenderSession, zoom: f32) -> Result<(), Error> {
if !Arc::ptr_eq(&self.measured_session.identity, &session.identity) {
return Err(Error::StaleRenderSession);
}
if !zoom.is_finite() || zoom != self.measured_zoom {
return Err(Error::StaleZoom);
}
let fonts = graphics_text::font_system()
.read()
.map_err(|_| Error::FontSystemUnavailable)?;
if fonts.version() != self.measured_session.version || fonts.version() != session.version {
return Err(Error::FontSetChanged);
}
Ok(())
}
pub fn displayed(&self, session: &RenderSession, zoom: f32) -> Result<&str, Error> {
self.check(session, zoom)?;
Ok(&self.displayed)
}
pub fn retained_source(
&self,
session: &RenderSession,
zoom: f32,
) -> Result<Range<usize>, Error> {
self.check(session, zoom)?;
Ok(self.retained_source.clone())
}
pub fn retains_any(
&self,
session: &RenderSession,
zoom: f32,
field: Range<usize>,
) -> Result<bool, Error> {
self.check(session, zoom)?;
if field.start > field.end || self.source.get(field.clone()).is_none() {
return Ok(false);
}
let end = field.end.min(self.retained_source.end);
Ok(field.start < end
&& self
.source
.get(field.start..end)
.is_some_and(|text| text.chars().any(|c| !c.is_whitespace())))
}
/// Validate finite world position and rectangle endpoints only. Camera
/// transforms and raster bounds remain a separate integration contract.
pub fn bounds_at(
&self,
session: &RenderSession,
zoom: f32,
position: Point,
) -> Result<Rectangle, Error> {
self.check(session, zoom)?;
translated_bounds(position, self.bounds.size())
}
/// Paint under the same positive uniform zoom used for `Cache::resolve`.
/// No per-label wrapping or implicit text maximum is introduced.
/// The returned Text must be consumed immediately in this session/zoom;
/// retaining raw Text past another font load/zoom change bypasses the guard.
pub fn canvas_text(
&self,
session: &RenderSession,
zoom: f32,
position: Point,
color: Color,
) -> Result<Text, Error> {
self.bounds_at(session, zoom, position)?;
Ok(Text {
content: self.displayed.clone(),
position,
color,
size: self.style.size,
line_height: self.style.line_height,
font: self.style.font,
shaping: self.style.shaping,
max_width: f32::INFINITY,
..Text::default()
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Error {
InvalidMetrics,
SourceBudget,
GraphemeBudget,
FontSystemUnavailable,
InvalidMeasurement,
InvalidPosition,
StaleZoom,
StaleRenderSession,
FontSetChanged,
}
struct Entry {
source: String,
style: Style,
zoom: f32,
width: f32,
label: Arc<Label>,
}
#[derive(Default)]
/// Two recent resolutions for ONE live labelled occurrence. A global small LRU
/// would thrash when a larger Scene is rebuilt in order. The integration owner
/// must retain these by occurrence identity and prune on removal/document change.
pub(super) struct Cache {
version: Option<graphics_text::Version>,
session: Option<Arc<()>>,
entries: VecDeque<Entry>,
#[cfg(test)]
allocations: usize,
}
impl Cache {
/// `width` excludes any caller-chosen backplate padding. Zero is valid.
/// Cache ownership must survive redraws; constructing one per Scene defeats
/// reuse. Changed fonts invalidate the SESSION: the caller must establish
/// a fresh renderer and a new settled session, not just remeasure this cache.
/// Error handling/fallback for these explicit experiment budgets is an
/// integration decision; never silently paint the old truncated contract.
pub fn resolve(
&mut self,
session: &RenderSession,
source: &str,
width: f32,
zoom: f32,
style: Style,
) -> Result<Arc<Label>, Error> {
let (size, line_height) = style.scaled(zoom);
if !width.is_finite()
|| width < 0.
|| !zoom.is_finite()
|| zoom <= 0.
|| !size.is_finite()
|| size <= 0.
|| !line_height.is_finite()
|| line_height <= 0.
|| !(width * zoom).is_finite()
|| !(size / zoom).is_finite()
|| !(line_height / zoom).is_finite()
{
return Err(Error::InvalidMetrics);
}
if source.len() > MAX_SOURCE_BYTES {
return Err(Error::SourceBudget);
}
let mut fonts = graphics_text::font_system()
.write()
.map_err(|_| Error::FontSystemUnavailable)?;
if session.version != fonts.version() {
return Err(Error::FontSetChanged);
}
if self.version != Some(fonts.version())
|| self
.session
.as_ref()
.is_none_or(|identity| !Arc::ptr_eq(identity, &session.identity))
{
self.entries.clear();
self.version = Some(fonts.version());
self.session = Some(session.identity.clone());
}
if let Some(index) = self.entries.iter().position(|entry| {
entry.source == source
&& entry.style == style
&& entry.zoom == zoom
&& entry.width == width
}) {
let entry = self.entries.remove(index).unwrap();
let label = entry.label.clone();
self.entries.push_back(entry);
return Ok(label);
}
let normalized = Normalized::new(source);
let cuts: Vec<usize> = normalized
.text
.grapheme_indices(true)
.map(|(i, _)| i)
.chain(std::iter::once(normalized.text.len()))
.collect();
if cuts.len().saturating_sub(1) > MAX_GRAPHEMES {
return Err(Error::GraphemeBudget);
}
// Candidate buffers are temporary. Only the resolved strings/metrics
// survive, bounded by MAX_ENTRIES; renderer caching remains independent.
let mut shapes = graphics_text::Cache::new();
let mut measure = |content: &str| -> Result<Size, Error> {
if content.is_empty() {
return Ok(Size::ZERO);
}
#[cfg(test)]
{
self.allocations += 1;
}
let (_, entry) = shapes.allocate(
fonts.raw(),
graphics_text::cache::Key {
content,
size,
line_height,
font: style.font,
bounds: Size::INFINITE,
shaping: style.shaping,
align_x: Alignment::Default,
},
);
if entry.buffer.layout_runs().count() != 1
|| !entry.min_bounds.width.is_finite()
|| !entry.min_bounds.height.is_finite()
{
return Err(Error::InvalidMeasurement);
}
world_size(entry.min_bounds, zoom)
};
let full = measure(&normalized.text)?;
let (displayed, retained, measured) = if full.width <= width {
(normalized.text.clone(), normalized.text.len(), full)
} else {
let ellipsis = measure("…")?;
if ellipsis.width > width {
(String::new(), 0, Size::ZERO)
} else {
// Every accepted candidate is actually shaped and fits. Search
// is bounded/logarithmic; contextual shaping is not universally
// monotone, so this does NOT promise the globally longest prefix.
// Full content is always checked first and never needlessly cut.
let mut best = ("…".to_owned(), 0, ellipsis);
let (mut low, mut high) = (1, cuts.len().saturating_sub(1));
while low < high {
let middle = low + (high - low) / 2;
let candidate = format!("{}…", &normalized.text[..cuts[middle]]);
let measured = measure(&candidate)?;
if measured.width <= width {
best = (candidate, cuts[middle], measured);
low = middle + 1;
} else {
high = middle;
}
}
best
}
};
let label = Arc::new(Label {
source: source.into(),
displayed,
retained_source: 0..normalized.source_end(retained),
bounds: Rectangle::with_size(measured),
style,
measured_session: session.clone(),
measured_zoom: zoom,
});
if self.entries.len() == MAX_ENTRIES {
self.entries.pop_front();
}
self.entries.push_back(Entry {
source: source.into(),
style,
zoom,
width,
label: label.clone(),
});
Ok(label)
}
}
fn world_size(scaled: Size, zoom: f32) -> Result<Size, Error> {
let world = Size::new(scaled.width / zoom, scaled.height / zoom);
if !world.width.is_finite()
|| !world.height.is_finite()
|| world.width < 0.
|| world.height < 0.
{
return Err(Error::InvalidMeasurement);
}
Ok(world)
}
fn translated_bounds(position: Point, size: Size) -> Result<Rectangle, Error> {
if !position.x.is_finite()
|| !position.y.is_finite()
|| !(position.x + size.width).is_finite()
|| !(position.y + size.height).is_finite()
{
return Err(Error::InvalidPosition);
}
Ok(Rectangle::new(position, size))
}
struct Normalized {
text: String,
ends: Vec<(usize, usize)>,
}
impl Normalized {
fn new(source: &str) -> Self {
let mut result = Self {
text: String::new(),
ends: vec![(0, 0)],
};
let mut separator = false;
for (start, c) in source.char_indices() {
if c.is_whitespace() {
separator = !result.text.is_empty();
continue;
}
if separator {
result.text.push(' ');
result.ends.push((result.text.len(), start));
separator = false;
}
result.text.push(c);
result.ends.push((result.text.len(), start + c.len_utf8()));
}
result
}
fn source_end(&self, normalized_end: usize) -> usize {
self.ends
.binary_search_by_key(&normalized_end, |(end, _)| *end)
.map(|index| self.ends[index].1)
.expect("grapheme cuts are character boundaries")
}
}
#[cfg(test)]
#[path = "scenario_label/tests.rs"]
mod tests;
@@ -0,0 +1,189 @@
//! Unwired corridor-policy candidate for the measured scenario-label experiment.
//!
//! The caller supplies a finite structural label band AFTER endpoint/activation
//! alignment, with padding already removed from its edges. Blockers include
//! padded activation/card/header bounds over the complete text band, including
//! stroke/ink clearance. This function does not infer a self-message corridor,
//! alter scene extents, change zoom, or establish raster-ink containment.
use iced::Rectangle;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct InvalidGeometry;
fn edges(rect: Rectangle) -> Result<(f32, f32, f32, f32), InvalidGeometry> {
let right = rect.x + rect.width;
let bottom = rect.y + rect.height;
if [rect.x, rect.y, rect.width, rect.height, right, bottom]
.into_iter()
.any(|value| !value.is_finite())
|| rect.width < 0.
|| rect.height < 0.
{
return Err(InvalidGeometry);
}
Ok((rect.x, right, rect.y, bottom))
}
/// Select one connected clear interval; never assemble text across blockers.
/// Prefer greater width, then proximity to the previous label anchor, then the
/// leftmost interval. Input order has no influence. A zero-area band has no
/// corridor. Touching the padded vertical band is conservatively obstructing;
/// a zero-area blocker has no area and must receive stroke padding upstream.
pub(super) fn widest_clear(
band: Rectangle,
blockers: &[Rectangle],
preferred_anchor: f32,
) -> Result<Option<Rectangle>, InvalidGeometry> {
let (left, right, top, bottom) = edges(band)?;
if !preferred_anchor.is_finite() {
return Err(InvalidGeometry);
}
let mut blocked = Vec::new();
for &blocker in blockers {
let (x0, x1, y0, y1) = edges(blocker)?;
if blocker.width > 0. && blocker.height > 0. && y1 >= top && y0 <= bottom {
let interval = (x0.max(left), x1.min(right));
if interval.0 < interval.1 {
blocked.push(interval);
}
}
}
if band.width == 0. || band.height == 0. {
return Ok(None);
}
blocked.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
let mut best: Option<Rectangle> = None;
let mut consider = |start: f32, end: f32| {
if start >= end {
return;
}
let candidate = Rectangle {
x: start,
width: end - start,
..band
};
// Use f64 only for comparison distance: subtracting two admitted f32
// world coordinates can overflow even though each rectangle is finite.
let distance = |x: f32| (f64::from(x) - f64::from(preferred_anchor)).abs();
if best.is_none_or(|old| {
candidate.width > old.width
|| (candidate.width == old.width
&& (distance(candidate.x) < distance(old.x)
|| (distance(candidate.x) == distance(old.x) && candidate.x < old.x)))
}) {
best = Some(candidate);
}
};
let mut cursor = left;
for (start, end) in blocked {
consider(cursor, start);
cursor = cursor.max(end);
}
consider(cursor, right);
Ok(best)
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x: f32, y: f32, width: f32, height: f32) -> Rectangle {
Rectangle {
x,
y,
width,
height,
}
}
#[test]
fn long_message_does_not_cross_an_intermediate_execution() {
let band = rect(20., 100., 700., 22.);
let blockers = [rect(250., 80., 16., 80.), rect(490., 90., 28., 140.)];
let result = widest_clear(band, &blockers, band.x).unwrap().unwrap();
assert_eq!(result, rect(20., 100., 230., 22.));
for blocker in blockers {
assert!(result.x + result.width <= blocker.x || result.x >= blocker.x + blocker.width);
}
assert_eq!(widest_clear(band, &[], band.x).unwrap(), Some(band));
}
#[test]
fn overlapping_unsorted_blockers_merge_and_widest_gap_wins() {
let band = rect(-100., 10., 600., 20.);
let blockers = [
rect(50., 0., 80., 60.),
rect(0., 5., 70., 30.),
rect(450., 0., 100., 60.),
];
let expected = Some(rect(130., 10., 320., 20.));
assert_eq!(widest_clear(band, &blockers, -100.).unwrap(), expected);
let mut reversed = blockers;
reversed.reverse();
assert_eq!(widest_clear(band, &reversed, -100.).unwrap(), expected);
}
#[test]
fn complete_padded_band_and_touching_boundaries_are_obstructed() {
let band = rect(0., 100., 200., 20.);
// The first blocker ends at the band top: checking only a baseline
// would miss it. The second starts at the band bottom.
let blockers = [rect(0., 70., 60., 30.), rect(150., 120., 50., 30.)];
assert_eq!(
widest_clear(band, &blockers, 0.).unwrap(),
Some(rect(60., 100., 90., 20.))
);
let outside = [rect(0., 70., 200., 29.), rect(0., 121., 200., 30.)];
assert_eq!(widest_clear(band, &outside, 0.).unwrap(), Some(band));
}
#[test]
fn ties_keep_the_nearest_anchor_then_choose_leftmost() {
let band = rect(0., 0., 300., 20.);
let blockers = [rect(100., 0., 100., 20.)];
assert_eq!(
widest_clear(band, &blockers, 190.).unwrap(),
Some(rect(200., 0., 100., 20.))
);
assert_eq!(
widest_clear(band, &blockers, 100.).unwrap(),
Some(rect(0., 0., 100., 20.))
);
}
#[test]
fn caller_defined_self_and_reversed_corridors_keep_their_geometry() {
// Direction and self-loop policy are explicit caller inputs. Neither
// a zero endpoint gap nor screen width can silently supply a corridor.
for band in [rect(420., 40., 180., 22.), rect(-400., 90., 350., 22.)] {
assert_eq!(widest_clear(band, &[], band.x).unwrap(), Some(band));
}
assert_eq!(
widest_clear(rect(420., 40., 0., 22.), &[], 420.).unwrap(),
None
);
assert_eq!(
widest_clear(rect(420., 40., 180., 0.), &[], 420.).unwrap(),
None
);
assert_eq!(
widest_clear(rect(0., 0., 100., 20.), &[rect(-10., -10., 200., 50.)], 0.).unwrap(),
None
);
}
#[test]
fn invalid_or_overflowing_geometry_never_enters_a_corridor() {
let band = rect(0., 0., 100., 20.);
for invalid in [
rect(f32::NAN, 0., 10., 20.),
rect(0., 0., -1., 20.),
rect(f32::MAX, 0., f32::MAX, 20.),
rect(0., f32::MAX, 10., f32::MAX),
] {
assert_eq!(widest_clear(invalid, &[], 0.), Err(InvalidGeometry));
assert_eq!(widest_clear(band, &[invalid], 0.), Err(InvalidGeometry));
}
assert_eq!(widest_clear(band, &[], f32::INFINITY), Err(InvalidGeometry));
}
}
@@ -0,0 +1,487 @@
//! Unexecuted evaluator candidates; the module is intentionally not wired yet.
use super::*;
use iced::advanced::{
graphics::text::Paragraph,
text::{Paragraph as _, Text as ParagraphText, Wrapping},
};
// Independent paragraph construction: no candidate cache, normalization, search,
// or character-width estimate is used by this measurement oracle.
fn paragraph_size(content: &str, zoom: f32, style: Style) -> Size {
let size = Pixels(style.size.0 * zoom);
let line_height = match style.line_height {
LineHeight::Relative(factor) => LineHeight::Relative(factor),
LineHeight::Absolute(pixels) => LineHeight::Absolute(Pixels(pixels.0 * zoom)),
};
let paragraph = Paragraph::with_text(ParagraphText {
content,
bounds: Size::INFINITE,
size,
line_height,
font: style.font,
align_x: Alignment::Default,
align_y: iced::alignment::Vertical::Top,
shaping: style.shaping,
wrapping: Wrapping::default(),
});
assert_eq!(paragraph.buffer().layout_runs().count(), 1);
Size::new(paragraph.min_width() / zoom, paragraph.min_height() / zoom)
}
#[test]
fn adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let style = Style::default();
for text in [
"1. call · Request valve repair · Technician service conversation",
"2. reply · Return pressure after repair · Technician service conversation",
"1. call · Request redundant hydraulic pressure calibration channel A",
"1. call · Request redundant hydraulic pressure calibration channel B",
] {
let wide = cache.resolve(&session, text, 1600., 1., style).unwrap();
assert_eq!(wide.displayed(&session, 1.).unwrap(), text);
assert_eq!(wide.retained_source(&session, 1.).unwrap(), 0..text.len());
assert_eq!(wide.bounds.size(), paragraph_size(text, 1., style));
let narrow = cache.resolve(&session, text, 80., 1., style).unwrap();
assert_ne!(narrow.displayed, text);
assert!(narrow.bounds.width <= 80.);
assert_eq!(
narrow.bounds.size(),
paragraph_size(&narrow.displayed, 1., style)
);
}
}
#[test]
fn normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation() {
let raw = " \t1. call · Pump\r\n · Operation: Need\u{301}le ";
let expected = "1. call · Pump · Operation: Need\u{301}le";
let n = Normalized::new(raw);
assert_eq!(n.text, expected);
let prefix = "1. call · Pump ";
assert_eq!(n.source_end(prefix.len()), raw.find("· Operation").unwrap());
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let wide = cache
.resolve(&session, raw, 1600., 1., Style::default())
.unwrap();
assert_eq!(wide.displayed, expected);
assert_eq!(wide.retained_source.end, raw.trim_end().len());
let field = raw.find("Need").unwrap()..raw.trim_end().len();
assert!(wide.retains_any(&session, 1., field.clone()).unwrap());
assert!(
!wide.retains_any(&session, 1., 0..2).unwrap(),
"normalized whitespace is not a visible field"
);
let small = cache
.resolve(&session, raw, 70., 1., Style::default())
.unwrap();
assert!(!small.retains_any(&session, 1., field).unwrap());
assert!(small.retained_source.end < raw.find("Operation").unwrap());
}
#[test]
fn grapheme_prefixes_and_zoom_match_actual_paragraph_layout() {
let text = "call · 戻り値 圧力 e\u{301} 👩‍🔬 safety valve verification";
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for zoom in [0.66, 1., 1.75] {
let label = cache
.resolve(&session, text, 105., zoom, Style::default())
.unwrap();
assert!(label.bounds.width <= 105.);
assert_eq!(
label.bounds.size(),
paragraph_size(&label.displayed, zoom, Style::default())
);
assert!(
text.grapheme_indices(true)
.any(|(i, _)| i == label.retained_source.end)
|| label.retained_source.end == text.len()
);
let painted = label
.canvas_text(&session, zoom, Point::new(400., 90.), Color::BLACK)
.unwrap();
assert_eq!(painted.content, label.displayed);
assert!(painted.max_width.is_infinite());
assert_eq!(
label
.bounds_at(&session, zoom, painted.position)
.unwrap()
.size(),
label.bounds.size()
);
}
}
#[test]
fn empty_and_too_narrow_labels_never_claim_source_visibility() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for text in ["", " \t\n", "Operation: Pump"] {
let label = cache
.resolve(&session, text, 0., 1., Style::default())
.unwrap();
assert_eq!(label.displayed, "");
assert_eq!(label.retained_source, 0..0);
assert_eq!(label.bounds.size(), Size::ZERO);
assert!(!label.retains_any(&session, 1., 0..text.len()).unwrap());
}
}
#[test]
fn redraws_reuse_resolution_but_width_zoom_and_style_are_dependencies() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Pressure verification with a descriptive operation label";
let style = Style::default();
let a = cache.resolve(&session, text, 100., 1., style).unwrap();
let measured = cache.allocations;
let b = cache.resolve(&session, text, 100., 1., style).unwrap();
assert!(Arc::ptr_eq(&a, &b));
assert_eq!(cache.allocations, measured);
for (width, zoom, style) in [
(120., 1., style),
(100., 0.66, style),
(
100.,
1.,
Style {
size: Pixels(14.),
..style
},
),
] {
let other = cache.resolve(&session, text, width, zoom, style).unwrap();
assert!(!Arc::ptr_eq(&a, &other));
}
}
#[test]
fn resident_entry_is_remeasured_for_a_fresh_settled_render_session() {
let mut cache = Cache::default();
let old_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Pressure verification";
let a = cache
.resolve(&old_session, text, 100., 1., Style::default())
.unwrap();
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
let measured = cache.allocations;
assert!(measured > 0);
assert!(Arc::ptr_eq(
&a,
&cache
.resolve(&old_session, text, 100., 1., Style::default())
.unwrap()
));
assert_eq!(cache.allocations, measured);
// A fresh renderer session invalidates a still-resident entry. This does
// not mutate global fonts or purport to test dynamic WGPU font loading.
let new_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let refreshed = cache
.resolve(&new_session, text, 100., 1., Style::default())
.unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
assert!(cache.allocations > measured);
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(
&cache.entries.front().unwrap().label,
&refreshed
));
}
#[test]
fn resident_entry_remeasures_after_local_font_epoch_invalidation() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Resident pressure label";
let a = cache
.resolve(&session, text, 100., 1., Style::default())
.unwrap();
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
let measured = cache.allocations;
assert!(measured > 0);
// Exercise the local memo epoch branch while a is still resident. No
// intervening width/zoom/style resolution can cause incidental eviction.
// This is not an actual font-load or renderer-cache acceptance test.
cache.version = None;
let refreshed = cache
.resolve(&session, text, 100., 1., Style::default())
.unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
assert!(cache.allocations > measured);
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(
&cache.entries.front().unwrap().label,
&refreshed
));
}
#[test]
fn retained_artifact_rejects_other_zoom_and_render_session_for_every_consumer() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let label = cache
.resolve(&session, "Operation: Pressure", 200., 1., Style::default())
.unwrap();
let other = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for (context, zoom, expected) in [
(&session, 0.66, Error::StaleZoom),
(&other, 1., Error::StaleRenderSession),
] {
assert_eq!(label.displayed(context, zoom).unwrap_err(), expected);
assert_eq!(label.retained_source(context, zoom).unwrap_err(), expected);
assert_eq!(
label.retains_any(context, zoom, 0..9).unwrap_err(),
expected
);
assert_eq!(
label.bounds_at(context, zoom, Point::ORIGIN).unwrap_err(),
expected
);
assert_eq!(
label
.canvas_text(context, zoom, Point::ORIGIN, Color::BLACK)
.unwrap_err(),
expected
);
}
assert!(
label
.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK)
.is_ok()
);
}
#[test]
fn finite_scaled_metrics_cannot_escape_as_infinite_world_dimensions() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let style = Style {
size: Pixels(2.),
line_height: LineHeight::Relative(f32::MAX),
..Style::default()
};
// At zoom 0.5 the scaled size=1 and line height=MAX are finite. The
// required world line height is not; reject before passing it to shaping.
let scaled = style.scaled(0.5);
assert!(scaled.0.is_finite() && scaled.1.is_finite());
assert_eq!(
cache
.resolve(&session, "call", 100., 0.5, style)
.unwrap_err(),
Error::InvalidMetrics
);
assert_eq!(cache.allocations, 0);
assert!(cache.entries.is_empty());
// Independently exercise validation after division for actual measured
// dimensions: finite scaled width/height do not imply finite world values.
assert_eq!(
world_size(Size::new(f32::MAX, 1.), 0.5),
Err(Error::InvalidMeasurement)
);
assert_eq!(
world_size(Size::new(1., f32::MAX), 0.5),
Err(Error::InvalidMeasurement)
);
assert_eq!(
world_size(Size::new(20., 8.), 0.5).unwrap(),
Size::new(40., 16.)
);
}
#[test]
fn malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for (width, zoom) in [(f32::NAN, 1.), (-1., 1.), (100., 0.), (100., f32::INFINITY)] {
assert_eq!(
cache
.resolve(&session, "call", width, zoom, Style::default())
.unwrap_err(),
Error::InvalidMetrics
);
}
assert_eq!(
cache
.resolve(
&session,
&"x".repeat(MAX_SOURCE_BYTES + 1),
100.,
1.,
Style::default()
)
.unwrap_err(),
Error::SourceBudget
);
assert_eq!(
cache
.resolve(
&session,
&"x".repeat(MAX_GRAPHEMES + 1),
100.,
1.,
Style::default()
)
.unwrap_err(),
Error::GraphemeBudget
);
assert!(cache.entries.is_empty());
}
#[test]
fn bounds_translation_rejects_nonfinite_world_positions_and_endpoints() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let label = cache
.resolve(&session, "call", 100., 1., Style::default())
.unwrap();
for position in [Point::new(f32::NAN, 0.), Point::new(0., f32::INFINITY)] {
assert_eq!(
label.bounds_at(&session, 1., position).unwrap_err(),
Error::InvalidPosition
);
assert_eq!(
label
.canvas_text(&session, 1., position, Color::BLACK)
.unwrap_err(),
Error::InvalidPosition
);
}
assert_eq!(
translated_bounds(Point::new(f32::MAX, 0.), Size::new(f32::MAX, 1.)),
Err(Error::InvalidPosition)
);
assert_eq!(
translated_bounds(Point::new(0., f32::MAX), Size::new(1., f32::MAX)),
Err(Error::InvalidPosition)
);
assert_eq!(
label
.bounds_at(&session, 1., Point::new(-20., -10.))
.unwrap()
.position(),
Point::new(-20., -10.)
);
}
/// Must run as the ONLY selected test in a dedicated, fresh test process.
/// The ignored marker keeps this global-font mutation out of normal parallel
/// desktop tests. See the evidence directory's exact executable runner.
#[test]
#[ignore = "mutates the global font database; run only this exact test in a fresh process"]
fn actual_font_load_rejects_retained_label_and_old_session() {
use graphics_text::cosmic_text::fontdb;
use std::borrow::Cow;
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let source = "Operation: Pressure";
let label = cache
.resolve(&session, source, 200., 1., Style::default())
.unwrap();
assert_eq!(label.displayed(&session, 1.).unwrap(), source);
assert!(
label
.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK)
.is_ok()
);
let measured = cache.allocations;
assert!(measured > 0);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &label));
let (version_before, version_after, faces_before, faces_after) = {
let mut fonts = graphics_text::font_system().write().unwrap();
let face = fonts
.raw()
.db()
.faces()
.find(|face| {
face.families
.iter()
.any(|(family, _)| family == "Iced-Icons")
})
.expect("Iced 0.14 embeds and loads its known Iced-Icons.ttf face")
.id;
let bytes = fonts
.raw()
.db()
.with_face_data(face, |data, _| data.to_vec())
.expect("the built-in binary font has accessible bytes");
assert!(!bytes.is_empty());
// load_font increments Version even for invalid bytes. Independently
// establish actual face admission so this cannot pass with invalid data.
let mut probe = fontdb::Database::new();
probe.load_font_data(bytes.clone());
let admitted = probe
.faces()
.filter(|face| {
face.families
.iter()
.any(|(family, _)| family == "Iced-Icons")
})
.count();
assert!(
admitted > 0,
"the extracted bytes parse as the expected real font"
);
let before = fonts.version();
assert_eq!(before, session.version);
let count = fonts.raw().db().faces().count();
// Owned bytes intentionally bypass Borrowed-address deduplication.
// This is an actual valid duplicate-face load, not a claim the chosen
// face or rendered metrics change; the stale-epoch rule is the oracle.
fonts.load_font(Cow::Owned(bytes));
(
before,
fonts.version(),
count,
fonts.raw().db().faces().count(),
)
};
assert_ne!(version_before, version_after);
assert!(
faces_after > faces_before,
"actual font admission must accompany the version increment"
);
assert_eq!(
label.displayed(&session, 1.).unwrap_err(),
Error::FontSetChanged
);
assert_eq!(
label.retained_source(&session, 1.).unwrap_err(),
Error::FontSetChanged
);
assert_eq!(
label.retains_any(&session, 1., 0..9).unwrap_err(),
Error::FontSetChanged
);
assert_eq!(
label.bounds_at(&session, 1., Point::ORIGIN).unwrap_err(),
Error::FontSetChanged
);
assert_eq!(
label
.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK)
.unwrap_err(),
Error::FontSetChanged
);
assert_eq!(
cache
.resolve(&session, source, 200., 1., Style::default())
.unwrap_err(),
Error::FontSetChanged
);
assert_eq!(
cache.allocations, measured,
"an invalid session must not remeasure and imply renderer repair"
);
println!(
"Valid Iced-Icons load: faces {faces_before}->{faces_after}, font Version {version_before:?}->{version_after:?}; old label and session rejected"
);
// Do not create a fresh session or claim dynamic WGPU cache recovery here.
// Process exit is the isolation boundary; no other test may run afterward.
}
+17 -1
View File
@@ -10,6 +10,10 @@ use std::{fmt, sync::Arc};
const HISTORY_LIMIT: usize = 100;
const HISTORY_BYTES: usize = 32 * 1024 * 1024;
#[path = "document_alias_rename.rs"]
mod alias_rename;
pub use alias_rename::PreparedAliasRename;
struct HistoryEntry {
snapshot: Arc<CompiledSourceProject>,
bytes: usize,
@@ -91,6 +95,8 @@ impl From<Vec<SourceDiagnostic>> for DocumentError {
}
pub struct Document {
// Distinguishes equal accepted snapshots belonging to different documents.
alias_owner: Arc<()>,
current: Arc<CompiledSourceProject>,
past: Vec<HistoryEntry>,
future: Vec<HistoryEntry>,
@@ -121,6 +127,7 @@ impl Document {
let current = Arc::new(source_project::compile_bundle(&bundle, limits)?);
let current_bytes = estimated_bytes(&current);
Ok(Self {
alias_owner: Arc::new(()),
current,
current_bytes,
past: Vec::new(),
@@ -394,8 +401,17 @@ impl Document {
return false;
}
let bytes = estimated_bytes(&compiled);
self.commit_snapshot(Arc::new(compiled), bytes)
}
// Only validated internal paths supply a changed snapshot and its exact
// estimate. The ordinary commit wrapper checks bundle inequality; the alias
// path checks its private changed flag and exact accepted-snapshot authority.
// Alias preparation shares this algorithm while retaining the live save and
// history state, and does not clone/recompile the candidate on adoption.
fn commit_snapshot(&mut self, compiled: Arc<CompiledSourceProject>, bytes: usize) -> bool {
self.past.push(HistoryEntry {
snapshot: std::mem::replace(&mut self.current, Arc::new(compiled)),
snapshot: std::mem::replace(&mut self.current, compiled),
bytes: std::mem::replace(&mut self.current_bytes, bytes),
});
self.future.clear();
@@ -0,0 +1,233 @@
//! Alias-only worker preparation and guarded adoption. Public compiled DTOs do
//! not authorize adoption; only this module can mint a prepared rename.
use super::*;
use std::collections::BTreeMap;
/// A pinned intent minted by its owning Document. Move it to a worker and call
/// `prepare`; preparation has no access to the live Document or its histories.
#[derive(Debug)]
pub struct AliasRenameRequest {
owner: Arc<()>,
before: Arc<CompiledSourceProject>,
revision: u64,
limits: ProjectLimits,
target: Id,
old_alias: String,
new_alias: String,
}
#[derive(Debug)]
struct PreparedData {
request: AliasRenameRequest,
after: Arc<CompiledSourceProject>,
before_bytes: usize,
after_bytes: usize,
changed: bool,
}
/// Immutable, Clone-compatible transport for preview and approval. Neither
/// mutable candidate access nor deserialization can manufacture this authority.
#[derive(Debug, Clone)]
pub struct PreparedAliasRename(Arc<PreparedData>);
impl PreparedAliasRename {
pub fn before(&self) -> &CompiledSourceProject {
&self.0.request.before
}
pub fn after(&self) -> &CompiledSourceProject {
&self.0.after
}
pub fn target(&self) -> Id {
self.0.request.target
}
pub fn old_alias(&self) -> &str {
&self.0.request.old_alias
}
pub fn new_alias(&self) -> &str {
&self.0.request.new_alias
}
pub fn revision(&self) -> u64 {
self.0.request.revision
}
pub fn changed(&self) -> bool {
self.0.changed
}
}
fn refusal(message: &str) -> DocumentError {
ModelError(message.into()).into()
}
fn check_history_capacity(before_bytes: usize, after_bytes: usize) -> Result<(), DocumentError> {
if before_bytes.saturating_add(after_bytes) > HISTORY_BYTES {
return Err(refusal(
"This rename exceeds the model Undo budget. No rename was applied.",
));
}
Ok(())
}
impl Document {
pub fn alias_rename_request(
&self,
target: Id,
replacement: String,
) -> Result<AliasRenameRequest, DocumentError> {
if self.revision == u64::MAX {
return Err(refusal(
"The document revision is exhausted. Reopen before renaming.",
));
}
let old_alias = self
.project()
.identities()
.into_iter()
.find_map(|(id, alias)| (id == target).then(|| alias.to_owned()))
.ok_or_else(|| refusal("The alias target is no longer present."))?;
// A candidate has a nonzero estimate. Avoid expensive preparation when
// even the current snapshot leaves no room for its immediate Undo.
if old_alias != replacement && self.current_bytes >= HISTORY_BYTES {
return Err(refusal(
"This rename exceeds the model Undo budget. No rename was applied.",
));
}
Ok(AliasRenameRequest {
owner: self.alias_owner.clone(),
before: self.current.clone(),
revision: self.revision,
limits: self.limits,
target,
old_alias,
new_alias: replacement,
})
}
/// App must separately consume its current approval and check workspace,
/// invocation and draft freshness. This checks accepted Document authority.
pub fn commit_alias_rename(
&mut self,
prepared: &PreparedAliasRename,
) -> Result<bool, DocumentError> {
let data = &prepared.0;
let request = &data.request;
if self.revision == u64::MAX
|| self.revision != request.revision
|| !Arc::ptr_eq(&self.alias_owner, &request.owner)
|| !Arc::ptr_eq(&self.current, &request.before)
{
return Err(refusal(
"This rename preview is stale. Preview the current alias again.",
));
}
if !data.changed {
return Ok(false);
}
if self.current_bytes != data.before_bytes {
return Err(refusal(
"The rename history estimate is stale. Preview again.",
));
}
check_history_capacity(self.current_bytes, data.after_bytes)?;
Ok(self.commit_snapshot(data.after.clone(), data.after_bytes))
}
}
impl AliasRenameRequest {
/// Run on the preparation worker. The typed command and source updater are
/// the only candidate construction path, followed by alias-only invariants.
pub fn prepare(self) -> Result<PreparedAliasRename, DocumentError> {
let before_project = &self.before.decoded.project;
let candidate = prepare_command(
before_project,
&Command::SetAlias(self.target, self.new_alias.clone()),
)?;
let mut expected = before_project.clone();
assign_expected_alias(&mut expected, self.target, &self.new_alias)?;
let identities: BTreeMap<_, _> = before_project
.identities()
.into_iter()
.map(|(id, _)| (id, id))
.collect();
if candidate.project != expected || candidate.identity_map != identities {
return Err(refusal(
"Alias preparation changed unrelated model data or identities.",
));
}
let after = source_project::update_bundle_with_identity_map(
&self.before,
&candidate.project,
&candidate.identity_map,
self.limits,
)?;
let before = &self.before;
if after.decoded.project != expected
|| before.bundle.entry != after.bundle.entry
|| !before.bundle.files.keys().eq(after.bundle.files.keys())
|| before.index.lexical_owners != after.index.lexical_owners
|| !before
.index
.declarations
.iter()
.map(|(id, loc)| (id, &loc.path))
.eq(after
.index
.declarations
.iter()
.map(|(id, loc)| (id, &loc.path)))
{
return Err(refusal(
"Alias preparation changed source membership or ownership.",
));
}
let changed = before.bundle != after.bundle;
let before_bytes = estimated_bytes(before);
let after_bytes = estimated_bytes(&after);
if changed {
check_history_capacity(before_bytes, after_bytes)?;
}
Ok(PreparedAliasRename(Arc::new(PreparedData {
request: self,
after: Arc::new(after),
before_bytes,
after_bytes,
changed,
})))
}
}
// Independent full-model expectation: SetAlias may change exactly one header
// field, across every supported identity family. Do not reuse command execution
// to derive the expected result, which would hide collateral command changes.
fn assign_expected_alias(project: &mut Project, id: Id, alias: &str) -> Result<(), DocumentError> {
if project.id == id {
project.alias = alias.to_owned();
} else if let Some(record) = project.elements.get_mut(&id) {
record.alias = alias.to_owned();
} else if let Some(record) = project.relations.get_mut(&id) {
record.alias = alias.to_owned();
} else if let Some(record) = project.diagrams.get_mut(&id) {
record.alias = alias.to_owned();
} else if let Some(record) = project.artifact_links.get_mut(&id) {
record.alias = alias.to_owned();
} else if let Some(record) = project.traces.get_mut(&id) {
record.alias = alias.to_owned();
} else {
project
.diagrams
.values_mut()
.find_map(|diagram| diagram.nodes.get_mut(&id))
.ok_or_else(|| refusal("The alias target is no longer present."))?
.alias = alias.to_owned();
}
Ok(())
}
#[cfg(test)]
#[path = "document_alias_rename_tests.rs"]
mod tests;
@@ -0,0 +1,459 @@
use super::*;
fn fixture() -> Document {
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
("project.arcadia".into(), r#"arcadia 7;
project mission "Испытание" id "10000000-0000-4000-8000-000000000001" {
include "work.arcadia"; include "views.arcadia";
}
"#.replace('\n', "\r\n")),
("work.arcadia".into(), r#"// @assess and assess are explanatory prose.
oa_activity assess "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000002" { description "Keep @assess and assess"; }
oa_activity assist "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000003" { parent @ // owner evidence
assess; }
sa_function service "Service" id "10000000-0000-4000-8000-000000000004" {}
interaction transfer "Transfer" id "10000000-0000-4000-8000-000000000005" { source @assess; target @assist; }
trace review "Reviewed correspondence" id "10000000-0000-4000-8000-000000000006" { oa @assess; sa @service; kind Refinement; baseline { oa { name "Old assess"; description "@assess"; }; sa { name "Old service"; description "Kept"; }; }; }
artifact evidence "Evidence" id "10000000-0000-4000-8000-000000000007" { owner @review; uri "docs/assess.md"; }
"#.replace('\n', "\r\n")),
("views.arcadia".into(), r#"diagram overview "Overview" id "10000000-0000-4000-8000-000000000008" { layer Oa; routes [{ edge @ // routed identity
transfer; waypoints [{ x 4.20e2; y -16; }]; }]; }
node placement id "10000000-0000-4000-8000-000000000009" { diagram @overview; element @assess; position { x -0.0; y 12.375; }; size { width 260; height 94; }; }
"#.replace('\n', "\r\n")),
]),
};
let document = Document::from_bundle(bundle.clone(), ProjectLimits::default()).unwrap();
assert_eq!(
document.bundle(),
&bundle,
"fixture must need no migration or ID insertion"
);
document
}
fn id(number: u8) -> Id {
Id::parse_str(&format!("10000000-0000-4000-8000-{number:012}")).unwrap()
}
fn prepare(document: &Document, target: Id, replacement: &str) -> PreparedAliasRename {
document
.alias_rename_request(target, replacement.into())
.unwrap()
.prepare()
.unwrap()
}
// Capture every live Document state relevant to refusal/no-op. Arc identity also
// catches replacing an equal snapshot or saved marker behind the user's back.
struct Unchanged {
current: Arc<CompiledSourceProject>,
saved: Option<Arc<SourceBundle>>,
past: Vec<(Arc<CompiledSourceProject>, usize)>,
future: Vec<(Arc<CompiledSourceProject>, usize)>,
revision: u64,
current_bytes: usize,
recovered: bool,
}
impl Unchanged {
fn capture(document: &Document) -> Self {
Self {
current: document.snapshot(),
saved: document.saved.clone(),
past: document
.past
.iter()
.map(|h| (h.snapshot.clone(), h.bytes))
.collect(),
future: document
.future
.iter()
.map(|h| (h.snapshot.clone(), h.bytes))
.collect(),
revision: document.revision,
current_bytes: document.current_bytes,
recovered: document.recovered_unsaved,
}
}
fn assert(&self, document: &Document) {
assert!(Arc::ptr_eq(&self.current, &document.current));
match (&self.saved, &document.saved) {
(Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)),
(None, None) => {}
_ => panic!("saved baseline changed"),
}
for (before, after) in [
(&self.past, &document.past),
(&self.future, &document.future),
] {
assert_eq!(before.len(), after.len());
for ((snapshot, bytes), entry) in before.iter().zip(after) {
assert!(Arc::ptr_eq(snapshot, &entry.snapshot));
assert_eq!(*bytes, entry.bytes);
}
}
assert_eq!(self.revision, document.revision);
assert_eq!(self.current_bytes, document.current_bytes);
assert_eq!(self.recovered, document.recovered_unsaved);
}
}
#[test]
fn each_identity_family_changes_only_authored_alias_roles_and_one_history_entry() {
// Literal role-specific replacements are independent of the updater/tokenizer.
// They deliberately exclude prose, historical review text and URI spelling.
type AliasCase<'a> = (u8, &'a str, &'a str, &'a [(&'a str, &'a str)]);
let cases: &[AliasCase<'_>] = &[
(
1,
"mission",
"renamed",
&[("project mission ", "project renamed ")],
),
(
2,
"assess",
"renamed",
&[
("oa_activity assess ", "oa_activity renamed "),
("\r\n assess;", "\r\n renamed;"),
("@assess;", "@renamed;"),
],
),
(
5,
"transfer",
"renamed",
&[
("interaction transfer ", "interaction renamed "),
("\r\n transfer;", "\r\n renamed;"),
],
),
(
6,
"review",
"renamed",
&[
("trace review ", "trace renamed "),
("owner @review;", "owner @renamed;"),
],
),
(
7,
"evidence",
"renamed",
&[("artifact evidence ", "artifact renamed ")],
),
(
8,
"overview",
"renamed",
&[
("diagram overview ", "diagram renamed "),
("diagram @overview;", "diagram @renamed;"),
],
),
(
9,
"placement",
"renamed",
&[("node placement ", "node renamed ")],
),
];
for &(number, old, new, replacements) in cases {
let mut document = fixture();
let original = document.snapshot();
let mut expected_model = document.project().clone();
// Explicit per-family oracle, separate from production expectation helper.
match number {
1 => expected_model.alias = new.into(),
2 => expected_model.elements.get_mut(&id(2)).unwrap().alias = new.into(),
5 => expected_model.relations.get_mut(&id(5)).unwrap().alias = new.into(),
6 => expected_model.traces.get_mut(&id(6)).unwrap().alias = new.into(),
7 => expected_model.artifact_links.get_mut(&id(7)).unwrap().alias = new.into(),
8 => expected_model.diagrams.get_mut(&id(8)).unwrap().alias = new.into(),
9 => {
expected_model
.diagrams
.get_mut(&id(8))
.unwrap()
.nodes
.get_mut(&id(9))
.unwrap()
.alias = new.into()
}
_ => unreachable!(),
}
let mut expected_bundle = document.bundle().clone();
for source in expected_bundle.files.values_mut() {
for &(before, after) in replacements {
*source = source.replace(before, after);
}
}
let state = Unchanged::capture(&document);
let prepared = prepare(&document, id(number), new);
state.assert(&document);
assert!(prepared.changed());
assert_eq!(prepared.old_alias(), old);
assert_eq!(prepared.new_alias(), new);
assert_eq!(prepared.target(), id(number));
assert_eq!(prepared.revision(), 0);
assert_eq!(prepared.before().bundle, original.bundle);
assert_eq!(prepared.after().bundle, expected_bundle);
assert_eq!(prepared.after().decoded.project, expected_model);
assert!(document.commit_alias_rename(&prepared).unwrap());
assert!(
Arc::ptr_eq(&document.current, &prepared.0.after),
"adopt the exact prepared Arc"
);
assert_eq!(document.past.len(), 1);
assert!(document.dirty());
let after = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&prepared.clone()).is_err());
after.assert(&document);
assert!(document.undo());
assert!(Arc::ptr_eq(&document.current, &original));
assert!(!document.dirty());
assert!(!document.can_undo());
assert!(document.redo());
assert_eq!(document.bundle(), &expected_bundle);
assert_eq!(document.project(), &expected_model);
document.mark_saved(document.snapshot());
let reopened =
Document::from_bundle(document.bundle().clone(), ProjectLimits::default()).unwrap();
assert_eq!(reopened.project(), &expected_model);
assert_eq!(reopened.bundle(), &expected_bundle);
assert!(!reopened.dirty());
}
}
#[test]
fn highlighted_keyword_alias_is_valid_without_normalizing_input() {
assert!(matches!(
language::tokenize("oa_activity")[0].class,
language::TokenClass::Keyword
));
let mut document = fixture();
let prepared = prepare(&document, id(2), "oa_activity");
document.commit_alias_rename(&prepared).unwrap();
assert_eq!(document.project().elements[&id(2)].alias, "oa_activity");
assert_eq!(document.project().elements[&id(2)].name, "Оценка ущерба");
}
#[test]
fn invalid_and_identical_aliases_preserve_live_state_and_redo() {
let mut document = fixture();
let first = prepare(&document, id(2), "changed");
document.commit_alias_rename(&first).unwrap();
assert!(document.undo());
document.invalidate_saved_baseline();
document.mark_recovered();
let state = Unchanged::capture(&document);
for replacement in ["", "has space", "project", "assist", " assess", "assess "] {
assert!(
document
.alias_rename_request(id(2), replacement.into())
.unwrap()
.prepare()
.is_err()
);
state.assert(&document);
}
assert!(
document
.alias_rename_request(Id::nil(), "new".into())
.is_err()
);
let no_op = prepare(&document, id(2), "assess");
assert!(!no_op.changed());
assert!(!document.commit_alias_rename(&no_op).unwrap());
state.assert(&document);
assert!(document.redo());
assert_eq!(document.project().elements[&id(2)].alias, "changed");
}
#[test]
fn equal_bytes_undo_replay_cross_document_and_exhausted_revision_refuse() {
let mut document = fixture();
let old = prepare(&document, id(2), "renamed");
let mut second = fixture();
let second_state = Unchanged::capture(&second);
assert!(second.commit_alias_rename(&old).is_err());
second_state.assert(&second);
// Even an identical shared accepted Arc cannot confer this Document's owner.
second.current = document.current.clone();
let second_state = Unchanged::capture(&second);
assert!(second.commit_alias_rename(&old).is_err());
second_state.assert(&second);
document
.execute(Command::Rename(id(2), "Later name".into()))
.unwrap();
let edited = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&old).is_err());
edited.assert(&document);
assert!(document.undo());
assert!(Arc::ptr_eq(&document.current, &old.0.request.before));
let undone = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&old).is_err());
undone.assert(&document);
document.revision = u64::MAX - 1;
let last = prepare(&document, id(2), "renamed");
document
.execute(Command::Rename(id(2), "Exhaust revision".into()))
.unwrap();
document.undo();
assert!(Arc::ptr_eq(&document.current, &last.0.request.before));
let exhausted = Unchanged::capture(&document);
assert!(document.commit_alias_rename(&last).is_err());
assert!(
document
.alias_rename_request(id(2), "renamed".into())
.is_err()
);
assert!(
document
.alias_rename_request(id(2), "assess".into())
.is_err()
);
exhausted.assert(&document);
}
#[test]
fn preview_retains_latest_saved_baseline_and_recovery_state() {
for baseline in ["saved_before", "unknown", "raw_repair", "recovered"] {
let mut document = fixture();
let prepared = prepare(&document, id(2), "renamed");
let original = document.snapshot();
match baseline {
"saved_before" => document.mark_saved(original.clone()),
"unknown" => document.invalidate_saved_baseline(),
"raw_repair" => {
let mut raw = document.bundle().clone();
raw.files
.insert("project.arcadia".into(), "invalid repair source".into());
document.bind_repair_baseline(raw);
}
"recovered" => document.mark_recovered(),
_ => unreachable!(),
}
let saved = document.saved.clone();
let recovered = document.recovered_unsaved;
assert!(document.commit_alias_rename(&prepared).unwrap());
assert_eq!(document.saved, saved);
assert_eq!(document.recovered_unsaved, recovered);
assert!(document.dirty());
assert!(document.undo());
assert_eq!(document.saved, saved);
assert_eq!(document.recovered_unsaved, recovered);
assert_eq!(document.dirty(), baseline != "saved_before");
}
// A completed save after Preview remains authoritative, including when it
// clears recovery; a worker must not restore its earlier state on Confirm.
let mut document = fixture();
document.mark_recovered();
document.invalidate_saved_baseline();
let prepared = prepare(&document, id(2), "renamed");
document.mark_saved(document.snapshot());
document.commit_alias_rename(&prepared).unwrap();
assert!(!document.recovered_unsaved);
document.undo();
assert!(!document.dirty());
// Undo from saved C to B, then a new B->C rename can correctly finish clean.
document.redo();
document.mark_saved(document.snapshot());
document.undo();
let return_to_saved = prepare(&document, id(2), "renamed");
document.commit_alias_rename(&return_to_saved).unwrap();
assert!(!document.dirty());
assert!(!document.can_redo());
}
#[test]
fn mutable_public_preview_copies_cannot_change_prepared_authority() {
let mut document = fixture();
let prepared = prepare(&document, id(2), "renamed");
let exact = prepared.after().clone();
let mut counterfeit = prepared.after().clone();
counterfeit.decoded.project.name = "Injected unrelated edit".into();
counterfeit.index.declarations.clear();
let unchanged = Unchanged::capture(&document);
assert!(document.apply_compiled(counterfeit).is_err());
unchanged.assert(&document);
let mut unrelated = exact.decoded.project.clone();
unrelated.name = "A self-consistent unrelated candidate".into();
let forged_dto = source_project::compile_bundle(
&source_project::format_bundle(&unrelated),
ProjectLimits::default(),
)
.unwrap();
assert_ne!(forged_dto.decoded.project, prepared.after().decoded.project);
// No API converts either DTO to PreparedAliasRename. Mutating owned copies
// leaves the opaque capability and its exact adopted model/source intact.
document.commit_alias_rename(&prepared).unwrap();
assert_eq!(document.project(), &exact.decoded.project);
assert_eq!(document.bundle(), &exact.bundle);
}
#[test]
fn exact_history_capacity_admits_immediate_undo_and_rejects_larger_candidate() {
let mut document = fixture();
let mut bundle = document.bundle().clone();
let padding = HISTORY_BYTES / 2 - document.current_bytes;
bundle
.files
.get_mut("views.arcadia")
.unwrap()
.push_str(&format!("//{}\r\n", "x".repeat(padding - 4)));
document.apply_bundle(bundle).unwrap();
assert_eq!(document.current_bytes, HISTORY_BYTES / 2);
// A same-length alias retains the exact estimate; older history is evictable.
let boundary = prepare(&document, id(2), "rating");
assert_eq!(
boundary.0.before_bytes + boundary.0.after_bytes,
HISTORY_BYTES
);
let unchanged = Unchanged::capture(&document);
assert!(
document
.alias_rename_request(id(2), "longer_alias".into())
.unwrap()
.prepare()
.is_err()
);
unchanged.assert(&document);
document.commit_alias_rename(&boundary).unwrap();
assert_eq!(document.past.len(), 1);
assert!(document.undo());
assert!(Arc::ptr_eq(&document.current, &boundary.0.request.before));
assert!(!document.can_undo());
assert!(document.redo());
assert!(Arc::ptr_eq(&document.current, &boundary.0.after));
assert!(check_history_capacity(usize::MAX, 1).is_err());
assert!(check_history_capacity(HISTORY_BYTES, 1).is_err());
}
#[test]
fn mint_and_commit_refuse_exhausted_or_inconsistent_live_history_estimate() {
let mut document = fixture();
let prepared = prepare(&document, id(2), "renamed");
// Inject cache corruption to falsify the commit-side check independently of
// the preparation admission. Normal edits also change revision and Arc.
document.current_bytes = HISTORY_BYTES;
let unchanged = Unchanged::capture(&document);
assert!(
document
.alias_rename_request(id(2), "renamed".into())
.is_err()
);
assert!(document.commit_alias_rename(&prepared).is_err());
// No-op intent needs no Undo reservation even at the admission boundary.
assert!(
document
.alias_rename_request(id(2), "assess".into())
.is_ok()
);
unchanged.assert(&document);
}
+10 -1
View File
@@ -805,7 +805,7 @@ fn article(topic: Topic) -> Article {
const {
Article {
summary: "The text editor and graphical forms edit the same model. Source projects preserve file organization and comments for human review, automation and version control.",
keywords: "text textual source language syntax highlighting lint parser git aliases uuid identity rename diff merge restructuredtext spec canonical parity definition references lexical navigation F12 back",
keywords: "text textual source language syntax highlighting lint parser git aliases uuid identity rename F2 preview diff merge restructuredtext spec canonical parity definition references lexical navigation F12 back",
sections: &[
walkthrough(
"Edit and review source",
@@ -834,6 +834,15 @@ fn article(topic: Topic) -> Article {
"Names, aliases and IDs serve different purposes",
"The displayed name is engineering prose. A stable @alias is the readable source reference and need not change when the name changes. UUID identity distinguishes the persistent record. Preserve IDs when revising the same engineering object; changing identity is a deliberate operation that affects references.",
),
walkthrough(
"Rename a source alias",
&[
"Place the caret on a declaration alias or live @reference, then choose Rename alias or press F2 in the source editor. First finish or revert pending source, property and behavior drafts; close or reattach any extra loaded detached files. Accepted edits do not need to be saved first.",
"Edit Source alias and choose Preview (Enter also previews). Pasting line breaks, tabs or other control characters is rejected without changing the field; choose Preview again after correcting the paste. Review the changed files, hunks and lines; long lines have additional segments. The engineering name and UUID stay unchanged. Comments and quoted prose keep their original spelling. This preview covers the rename; a later Save can include earlier accepted edits too.",
"Choose Rename alias to accept the complete preview, or Cancel/Escape to leave everything unchanged. An invalid, unchanged, stale or oversized preview cannot be confirmed. After editing the replacement, choose Preview again. If the source context changes, close the form and invoke Rename again on the current alias.",
"Use Undo rename immediately, or the model Undo/Redo controls, to restore the complete source/model snapshot. Ctrl+Z in Text still undoes source drafting. Save all and reopen use the normal project workflow; an unconfirmed preview is not saved.",
],
),
example(
"Readable references",
"For example, renaming an activity does not require renaming @assess_incident. A related record can still refer to that alias.",
+1
View File
@@ -25,6 +25,7 @@ mod recovery;
mod route_edit;
mod semantic_review;
mod source_navigation;
mod source_rename;
mod source_tree;
mod source_workspace;
mod style;
@@ -220,12 +220,22 @@ pub struct ReferenceRow {
pub evidence: Evidence,
}
/// Exact lexical occurrence that answered the query, independent of row limits.
/// The optional @ marker is separate from the alias; comments between them do
/// not become part of either hit.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Origin {
pub span: Span,
pub marker: Option<Span>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Results {
pub stamp: QueryStamp,
pub action: Action,
pub origin_path: String,
pub origin_offset: usize,
pub origin: Option<Origin>,
pub alias: Option<String>,
pub scope: Scope,
pub disposition: Disposition,
@@ -823,10 +833,28 @@ impl Snapshot {
.ok_or(QueryError::MissingFile)?;
let index = self.index()?;
let hit = index.hit(file, query.byte_offset)?;
let alias = match hit {
Some(symbols::Hit::Definition(handle)) => Some(index.definition(handle)?.alias),
Some(symbols::Hit::Reference(handle)) => Some(index.reference(handle)?.alias),
None => None,
let (alias, origin) = match hit {
Some(symbols::Hit::Definition(handle)) => {
let definition = index.definition(handle)?;
(
Some(definition.alias),
Some(Origin {
span: definition.alias_location.span,
marker: None,
}),
)
}
Some(symbols::Hit::Reference(handle)) => {
let reference = index.reference(handle)?;
(
Some(reference.alias),
Some(Origin {
span: reference.alias_location.span,
marker: Some(reference.at.span),
}),
)
}
None => (None, None),
};
let mut budget = RowBudget {
rows: self.limits.result_rows,
@@ -840,6 +868,7 @@ impl Snapshot {
action: query.action,
origin_path: query.path.to_owned(),
origin_offset: query.byte_offset,
origin,
alias: alias.map(str::to_owned),
scope: self.scope.clone(),
disposition: Disposition::NoSymbol,
@@ -107,6 +107,65 @@ fn reference_offset(fixture: &Fixture) -> usize {
fixture.text("links.arcadia").find("source @").unwrap() + "source ".len()
}
#[test]
fn origin_keeps_exact_hit_when_display_rows_are_omitted() {
let mut fixture = Fixture::standard();
fixture
.text_mut("links.arcadia")
.push_str("// no @pump hit\r\n@pump @pump @ // intervening @pump\r\n pump");
let source = fixture.text("links.arcadia");
let start = source.rfind("pump").unwrap();
let marker = source.rfind("@ // intervening").unwrap();
let limits = Limits {
result_rows: 0,
..Limits::default()
};
let snapshot = fixture.snapshot(None, false, limits);
let expected = Some(Origin {
span: Span {
start,
end: start + 4,
},
marker: Some(Span {
start: marker,
end: marker + 1,
}),
});
for offset in [start, start + 3, marker] {
let result = query(&snapshot, "links.arcadia", offset, Action::References);
assert_eq!(result.origin, expected);
assert!(result.references.is_empty());
assert!(result.omitted_references > 2);
}
let comment = source.rfind("intervening @pump").unwrap() + "intervening @".len();
assert_eq!(
query(&snapshot, "links.arcadia", comment, Action::Definition).origin,
None
);
assert_eq!(
query(&snapshot, "links.arcadia", start + 4, Action::Definition).origin,
None
);
let definition_source = fixture.text("模块/活動.arcadia");
let definition_start = definition_source.find("pump").unwrap();
assert_eq!(
query(
&snapshot,
"模块/活動.arcadia",
definition_start,
Action::Definition
)
.origin,
Some(Origin {
span: Span {
start: definition_start,
end: definition_start + 4
},
marker: None
})
);
}
#[test]
fn exact_multifile_unicode_crlf_definition_and_repeated_lexical_references() {
let fixture = Fixture::standard();
+3
View File
@@ -0,0 +1,3 @@
//! Pure preview and exact origin mapping for the guarded alias-rename controller.
pub mod origin;
pub mod preview;
@@ -0,0 +1,278 @@
//! Map a role-proven alias occurrence through the exact prepared source change.
//! This is cursor restoration, not target resolution or commit authority.
use oa_core::language::{self, Outline, OutlineLimits, Span, TokenClass};
use std::fmt;
const LIMITS: OutlineLimits = OutlineLimits {
max_source_bytes: 64 * 1024 * 1024,
max_tokens: 1_000_000,
max_declarations: 250_000,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OriginError {
Budget,
InvalidSource,
MissingOrigin,
ChangedStructure,
}
impl fmt::Display for OriginError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Budget => "The alias cursor preview exceeds its source scan budget. No rename was applied.",
Self::InvalidSource => "The alias cursor could not be verified in this source. No rename was applied.",
Self::MissingOrigin => "The original alias occurrence no longer matches. Preview again; no rename was applied.",
Self::ChangedStructure => "The preview changed source outside the expected alias tokens. No rename was applied.",
})
}
}
impl std::error::Error for OriginError {}
fn word(class: TokenClass) -> bool {
matches!(class, TokenClass::Identifier | TokenClass::Keyword)
}
/// The controller passes the exact alias span returned by the admitted symbol
/// hit. `marker` is Some only for a reference (its separate @ span), including
/// invocation with the caret on @. A declaration passes None.
pub fn map(
before: &str,
after: &str,
old_alias: &str,
new_alias: &str,
origin: Span,
marker: Option<Span>,
) -> Result<Span, OriginError> {
let old = language::outline(before, LIMITS).map_err(|_| OriginError::Budget)?;
let new = language::outline(after, LIMITS).map_err(|_| OriginError::Budget)?;
if old.has_lexical_errors || new.has_lexical_errors {
return Err(OriginError::InvalidSource);
}
if old.tokens.len() != new.tokens.len() {
return Err(OriginError::ChangedStructure);
}
let roles_before = roles(&old);
let roles_after = roles(&new);
let mut mapped = None;
for (index, (a, b)) in old.tokens.iter().zip(&new.tokens).enumerate() {
let left = &before[a.span.start..a.span.end];
let right = &after[b.span.start..b.span.end];
let alias_pair = word(a.class)
&& word(b.class)
&& left == old_alias
&& right == new_alias
&& roles_before[index].is_some()
&& roles_after[index].is_some()
&& roles_before[index].map(|role| role.is_some())
== roles_after[index].map(|role| role.is_some());
if a.span == origin {
if !alias_pair || roles_before[index] != Some(marker) {
return Err(OriginError::MissingOrigin);
}
mapped = Some(b.span);
}
if !alias_pair && (left != right || a.class != b.class) {
return Err(OriginError::ChangedStructure);
}
}
mapped.ok_or(OriginError::MissingOrigin)
}
/// Outer None is not an alias role; Some(None) is a declaration alias;
/// Some(Some(span)) is a live @ reference with its exact separate marker.
/// Scan sorted declaration spans and tokens together, without repeated searches.
fn roles(outline: &Outline<'_>) -> Vec<Option<Option<Span>>> {
let mut result = vec![None; outline.tokens.len()];
let mut declarations = outline.declarations.iter().peekable();
let mut significant = None;
for (index, token) in outline.tokens.iter().enumerate() {
while declarations
.peek()
.is_some_and(|d| d.alias_span.start < token.span.start)
{
declarations.next();
}
if word(token.class) {
if declarations
.peek()
.is_some_and(|d| d.alias_span == token.span)
{
result[index] = Some(None);
} else if let Some(previous) = significant {
let span: Span = previous;
if &outline.source[span.start..span.end] == "@" {
result[index] = Some(Some(span));
}
}
}
if !matches!(token.class, TokenClass::Whitespace | TokenClass::Comment) {
significant = Some(token.span);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn occurrence(source: &str, needle: &str, which: usize) -> Span {
let start = source.match_indices(needle).nth(which).unwrap().0;
Span {
start,
end: start + needle.len(),
}
}
#[test]
fn maps_last_repeated_reference_with_unicode_crlf_and_comment_separator() {
let before =
"oa_activity a \"救援\" {}\r\n// @a is prose\r\nsource @a; target @ // a\r\n a;\r\n";
let after = "oa_activity renamed \"救援\" {}\r\n// @a is prose\r\nsource @renamed; target @ // a\r\n renamed;\r\n";
let start = before.rfind(" a;").unwrap() + 1;
let marker = before.rfind("@ //").unwrap();
let mapped = map(
before,
after,
"a",
"renamed",
Span {
start,
end: start + 1,
},
Some(Span {
start: marker,
end: marker + 1,
}),
)
.unwrap();
assert_eq!(&after[mapped.start..mapped.end], "renamed");
assert_eq!(mapped.start, after.rfind(" renamed;").unwrap() + 1);
assert_eq!(
before[..start].bytes().filter(|b| *b == b'\n').count(),
after[..mapped.start]
.bytes()
.filter(|b| *b == b'\n')
.count()
);
}
#[test]
fn preserves_final_same_line_occurrence_not_first_or_prose() {
let before = "oa_activity a \"a @a\" {} source @a; target @a;";
let after = "oa_activity longer \"a @a\" {} source @longer; target @longer;";
let start = before.rfind("@a").unwrap();
let result = map(
before,
after,
"a",
"longer",
Span {
start: start + 1,
end: start + 2,
},
Some(Span {
start,
end: start + 1,
}),
)
.unwrap();
assert_eq!(result.start, after.rfind("@longer").unwrap() + 1);
let prose = occurrence(before, "a @a", 0);
assert!(
map(
before,
after,
"a",
"longer",
Span {
start: prose.start,
end: prose.start + 1
},
None
)
.is_err()
);
}
#[test]
fn permits_identifier_keyword_transitions_in_alias_roles() {
let before = "oa_activity a \"Name\" {} source @a;";
let alias = occurrence(before, "a \"Name", 0);
let origin = Span {
start: alias.start,
end: alias.start + 1,
};
for replacement in ["none", "include", "oa_activity"] {
assert!(oa_core::valid_alias(replacement));
assert_eq!(
language::tokenize(replacement)[0].class,
TokenClass::Keyword
);
let after = format!("oa_activity {replacement} \"Name\" {{}} source @{replacement};");
let mapped = map(before, &after, "a", replacement, origin, None).unwrap();
assert_eq!(&after[mapped.start..mapped.end], replacement);
assert_eq!(
map(&after, before, replacement, "a", mapped, None).unwrap(),
origin
);
}
}
#[test]
fn rejects_changed_trivia_non_alias_words_and_wrong_marker() {
let before = "oa_activity a \"Name\" {} source @a;";
let after = "oa_activity b \"Name\" {} source @b;";
let start = before.rfind("@a").unwrap();
let span = Span {
start: start + 1,
end: start + 2,
};
let marker = Some(Span {
start,
end: start + 1,
});
assert_eq!(
map(
before,
&after.replace("Name", "Other"),
"a",
"b",
span,
marker
),
Err(OriginError::ChangedStructure)
);
assert_eq!(
map(
before,
&after.replace(" source", " source"),
"a",
"b",
span,
marker
),
Err(OriginError::ChangedStructure)
);
assert_eq!(
map(before, after, "a", "b", span, None),
Err(OriginError::MissingOrigin)
);
assert!(
map(
before,
after,
"a",
"b",
Span {
start: 0,
end: usize::MAX
},
marker
)
.is_err()
);
}
}
@@ -0,0 +1,578 @@
//! Complete, bounded line-ordinal previews of an already prepared alias rename.
//!
//! Source text remains in the caller's immutable before/after snapshots. This
//! module owns only paths and indexes; it is not rename or commit authority.
use oa_core::source_project::SourceBundle;
use std::{fmt, ops::Range};
const CONTEXT: usize = 3;
// A byte-grid boundary can move back by at most three UTF-8 continuation bytes,
// so a 252-byte grid yields fragments of at most 255 bytes, independent of input.
const SEGMENT_GRID: usize = 252;
/// Count all bounded fragments of exact source text; an empty line has one
/// empty fragment. Page these as well as lines, never mount an unbounded line.
pub fn segment_count(text: &str) -> usize {
text.len().div_ceil(SEGMENT_GRID).max(1)
}
/// Constant-time UTF-8-safe page lookup. Concatenating every fragment gives the
/// original bytes, including CRLF. No ellipsis or truncation replaces content.
pub fn text_segment(text: &str, index: usize) -> Option<&str> {
if index >= segment_count(text) {
return None;
}
let mut start = index.checked_mul(SEGMENT_GRID)?;
let mut end = start.saturating_add(SEGMENT_GRID).min(text.len());
while !text.is_char_boundary(start) {
start -= 1;
}
while !text.is_char_boundary(end) {
end -= 1;
}
text.get(start..end)
}
const LIMITS: Limits = Limits {
files: 256,
changed_lines: 50_000,
display_bytes: 8 * 1024 * 1024,
};
#[derive(Clone, Copy)]
struct Limits {
files: usize,
changed_lines: usize,
display_bytes: usize,
}
#[derive(Debug)]
pub struct Preview {
files: Vec<File>,
changed_lines: usize,
display_bytes: usize,
}
#[derive(Debug)]
pub struct File {
path: String,
hunks: Vec<Hunk>,
}
#[derive(Debug)]
pub struct Hunk {
lines: Vec<Line>,
}
#[derive(Debug)]
pub struct Line {
number: usize,
before: Range<usize>,
after: Range<usize>,
changed: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum PreviewError {
MembershipChanged,
LineCountChanged(String),
LineEndingChanged { path: String, line: usize },
TooManyFiles { limit: usize },
TooManyChangedLines { limit: usize },
TooManyDisplayBytes { limit: usize },
}
impl fmt::Display for PreviewError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MembershipChanged => write!(
f,
"Rename preview refused: the entry or source file membership changed. No rename was applied."
),
Self::LineCountChanged(path) => write!(
f,
"Rename preview refused: the line count changed in {path}. No rename was applied."
),
Self::LineEndingChanged { path, line } => write!(
f,
"Rename preview refused: the line ending changed in {path}:{line}. No rename was applied."
),
Self::TooManyFiles { limit } => write!(
f,
"Rename preview exceeds {limit} changed files. A complete preview is required; no rename was applied."
),
Self::TooManyChangedLines { limit } => write!(
f,
"Rename preview exceeds {limit} changed source lines. A complete preview is required; no rename was applied."
),
Self::TooManyDisplayBytes { limit } => write!(
f,
"Rename preview exceeds {limit} display bytes (paths and both versions of changed/context lines). A complete preview is required; no rename was applied."
),
}
}
}
impl std::error::Error for PreviewError {}
impl Preview {
pub fn build(before: &SourceBundle, after: &SourceBundle) -> Result<Self, PreviewError> {
Self::with_limits(before, after, LIMITS)
}
fn with_limits(
before: &SourceBundle,
after: &SourceBundle,
limits: Limits,
) -> Result<Self, PreviewError> {
if before.entry != after.entry
|| !before.files.contains_key(&before.entry)
|| !before.files.keys().eq(after.files.keys())
{
return Err(PreviewError::MembershipChanged);
}
let mut preview = Self {
files: Vec::new(),
changed_lines: 0,
display_bytes: 0,
};
for ((path, old), (_, new)) in before.files.iter().zip(&after.files) {
if old == new {
continue;
}
if preview.files.len() == limits.files {
return Err(PreviewError::TooManyFiles {
limit: limits.files,
});
}
// A changed source line can contain arbitrarily many renamed tokens.
// Count the line once; this count is never a semantic-reference count.
let mut changed = Vec::new();
let mut old_lines = Lines::new(old);
let mut new_lines = Lines::new(new);
let mut count = 0;
loop {
match (old_lines.next(), new_lines.next()) {
(None, None) => break,
(Some(a), Some(b)) => {
count += 1;
if old[a.content_end..a.end] != new[b.content_end..b.end] {
return Err(PreviewError::LineEndingChanged {
path: path.clone(),
line: count,
});
}
if old[a.start..a.end] != new[b.start..b.end] {
if preview.changed_lines == limits.changed_lines {
return Err(PreviewError::TooManyChangedLines {
limit: limits.changed_lines,
});
}
preview.changed_lines += 1;
changed.push(count - 1);
}
}
_ => return Err(PreviewError::LineCountChanged(path.clone())),
}
}
// Merge touching context intervals in order, without pairwise diff.
// At most 7 indexed lines per changed line are retained; unchanged
// lines outside these intervals are scanned but never indexed.
let mut ranges: Vec<Range<usize>> = Vec::new();
for line in changed {
let range =
line.saturating_sub(CONTEXT)..line.saturating_add(CONTEXT + 1).min(count);
if let Some(previous) = ranges.last_mut()
&& range.start <= previous.end
{
previous.end = previous.end.max(range.end);
continue;
}
ranges.push(range);
}
preview.charge(path.len(), limits)?;
let mut file = File {
path: path.clone(),
hunks: Vec::new(),
};
let mut pairs = Lines::new(old).zip(Lines::new(new)).enumerate();
for range in ranges {
let mut hunk = Hunk { lines: Vec::new() };
for (ordinal, (a, b)) in pairs.by_ref() {
if ordinal < range.start {
continue;
}
// Charge exact UTF-8 bytes, including preserved endings and
// both copies of shared context, before adding the index.
// No render-time omission or shortening grants admission.
preview.charge(a.end - a.start, limits)?;
preview.charge(b.end - b.start, limits)?;
hunk.lines.push(Line {
number: ordinal + 1,
changed: old[a.start..a.end] != new[b.start..b.end],
before: a.start..a.end,
after: b.start..b.end,
});
if ordinal + 1 == range.end {
break;
}
}
file.hunks.push(hunk);
}
preview.files.push(file);
}
Ok(preview)
}
fn charge(&mut self, bytes: usize, limits: Limits) -> Result<(), PreviewError> {
let Some(total) = self.display_bytes.checked_add(bytes) else {
return Err(PreviewError::TooManyDisplayBytes {
limit: limits.display_bytes,
});
};
if total > limits.display_bytes {
return Err(PreviewError::TooManyDisplayBytes {
limit: limits.display_bytes,
});
}
self.display_bytes = total;
Ok(())
}
/// All admitted files in source path order; suitable for a file picker/search.
pub fn files(&self) -> &[File] {
&self.files
}
/// Changed source lines, not occurrences, declarations or semantic uses.
pub fn changed_line_count(&self) -> usize {
self.changed_lines
}
/// Exact displayed source bytes for both sides plus each stored path once.
/// Index metadata is separately bounded by 7 lines per changed line.
pub fn display_bytes(&self) -> usize {
self.display_bytes
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
impl File {
pub fn path(&self) -> &str {
&self.path
}
/// Every hunk is present; the caller may page these indexes, never truncate.
pub fn hunks(&self) -> &[Hunk] {
&self.hunks
}
}
impl Hunk {
/// The complete ordered line slice supports paging even a large single hunk.
pub fn lines(&self) -> &[Line] {
&self.lines
}
}
impl Line {
/// One-based source line number, identical on both sides.
pub fn number(&self) -> usize {
self.number
}
pub fn changed(&self) -> bool {
self.changed
}
/// Borrow from the immutable original file; includes its exact line ending.
pub fn before_text<'a>(&self, original: &'a str) -> Option<&'a str> {
original.get(self.before.clone())
}
/// Borrow from the immutable candidate file; includes its exact line ending.
pub fn after_text<'a>(&self, candidate: &'a str) -> Option<&'a str> {
candidate.get(self.after.clone())
}
}
struct RawLine {
start: usize,
content_end: usize,
end: usize,
}
struct Lines<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Lines<'a> {
fn new(text: &'a str) -> Self {
Self {
bytes: text.as_bytes(),
offset: 0,
}
}
}
impl Iterator for Lines<'_> {
type Item = RawLine;
fn next(&mut self) -> Option<Self::Item> {
if self.offset == self.bytes.len() {
return None;
}
let start = self.offset;
// The compiler and source navigation count LF, not bare CR, as a line
// boundary. Preserve CRLF as an ending and bare CR as source content.
while self.offset < self.bytes.len() && self.bytes[self.offset] != b'\n' {
self.offset += 1;
}
let mut content_end = self.offset;
if self.offset < self.bytes.len() {
if self.offset > start && self.bytes[self.offset - 1] == b'\r' {
content_end -= 1;
}
self.offset += 1;
}
Some(RawLine {
start,
content_end,
end: self.offset,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bundle(source: &str) -> SourceBundle {
SourceBundle {
entry: "救援.arcadia".into(),
files: [("救援.arcadia".into(), source.into())].into(),
}
}
#[test]
fn long_unicode_lines_are_fully_reachable_as_bounded_fragments() {
let text = format!("{}救援🙂{}\r\n", "a".repeat(251), "🙂".repeat(200));
let pieces: Vec<_> = (0..segment_count(&text))
.map(|i| text_segment(&text, i).unwrap())
.collect();
assert!(pieces.len() > 2);
assert!(pieces.iter().all(|part| part.len() <= 255));
assert_eq!(pieces.concat(), text);
assert_eq!(text_segment(&text, segment_count(&text)), None);
assert_eq!(text_segment(&text, usize::MAX), None);
assert_eq!(segment_count(""), 1);
assert_eq!(text_segment("", 0), Some(""));
}
#[test]
fn preserves_unicode_mixed_endings_and_counts_lines_not_repeated_uses() {
let old = bundle("// 救援\r\n@a @a\ncontext\r@a");
let new = bundle("// 救援\r\n@long @long\ncontext\r@long");
let preview = Preview::build(&old, &new).unwrap();
assert_eq!(preview.changed_line_count(), 2);
let file = &preview.files()[0];
assert_eq!(file.path(), "救援.arcadia");
assert_eq!(file.hunks().len(), 1);
let lines = file.hunks()[0].lines();
assert_eq!(
lines.iter().map(Line::number).collect::<Vec<_>>(),
[1, 2, 3]
);
assert_eq!(
lines[0].before_text(&old.files[&old.entry]),
Some("// 救援\r\n")
);
assert_eq!(
lines[1].after_text(&new.files[&new.entry]),
Some("@long @long\n")
);
assert_eq!(
lines[2].after_text(&new.files[&new.entry]),
Some("context\r@long")
);
assert!(lines[1].changed() && lines[2].changed());
assert!(!lines[0].changed());
assert_eq!(
preview.display_bytes(),
old.entry.len() + old.files[&old.entry].len() + new.files[&new.entry].len()
);
}
#[test]
fn first_and_last_lines_form_complete_separate_hunks_and_pages() {
let old = bundle("a\n1\n2\n3\n4\n5\n6\n7\n8\n9\na");
let new = bundle("long\n1\n2\n3\n4\n5\n6\n7\n8\n9\nlong");
let preview = Preview::build(&old, &new).unwrap();
let hunks = preview.files()[0].hunks();
assert_eq!(hunks.len(), 2);
let page_numbers: Vec<_> = hunks
.iter()
.flat_map(|h| h.lines().chunks(2))
.flat_map(|page| page.iter().map(Line::number))
.collect();
assert_eq!(page_numbers, [1, 2, 3, 4, 8, 9, 10, 11]);
assert_eq!(
hunks[1].lines()[3].after_text(&new.files[&new.entry]),
Some("long")
);
}
#[test]
fn nearby_changes_merge_and_all_changed_files_are_reachable() {
let mut old = bundle("a\n1\n2\n3\n4\n5\na\n");
let mut new = bundle("b\n1\n2\n3\n4\n5\nb\n");
old.files.insert("other.arcadia".into(), "a a a".into());
new.files.insert("other.arcadia".into(), "b b b".into());
let preview = Preview::build(&old, &new).unwrap();
assert_eq!(preview.changed_line_count(), 3);
assert_eq!(preview.files().len(), 2);
assert_eq!(preview.files()[0].path(), "other.arcadia");
assert_eq!(preview.files()[0].hunks()[0].lines().len(), 1);
assert_eq!(preview.files()[1].hunks().len(), 1);
assert_eq!(preview.files()[1].hunks()[0].lines().len(), 7);
}
#[test]
fn unchanged_and_empty_sources_have_no_preview() {
for source in ["", "a\r\nb\n", "a"] {
let original = bundle(source);
let preview = Preview::build(&original, &original).unwrap();
assert!(preview.is_empty());
assert_eq!(preview.changed_line_count(), 0);
assert_eq!(preview.display_bytes(), 0);
}
}
#[test]
fn refuses_membership_entry_line_count_and_line_ending_changes() {
let old = bundle("a\nb");
let mut new = old.clone();
new.files.insert("extra.arcadia".into(), "".into());
assert_eq!(
Preview::build(&old, &new).unwrap_err(),
PreviewError::MembershipChanged
);
let mut new = old.clone();
new.entry = "missing.arcadia".into();
assert_eq!(
Preview::build(&old, &new).unwrap_err(),
PreviewError::MembershipChanged
);
assert!(matches!(
Preview::build(&old, &bundle("a\nb\nc")),
Err(PreviewError::LineEndingChanged { .. }) | Err(PreviewError::LineCountChanged(_))
));
assert!(matches!(
Preview::build(&bundle("a\nb\n"), &bundle("a\n")),
Err(PreviewError::LineCountChanged(_))
));
for (a, b) in [("a\r\n", "a\n"), ("a\r", "a\n"), ("a", "a\n")] {
assert_eq!(
Preview::build(&bundle(a), &bundle(b)).unwrap_err(),
PreviewError::LineEndingChanged {
path: old.entry.clone(),
line: 1
}
);
}
}
#[test]
fn each_bound_is_exact_and_overflow_never_returns_a_partial_preview() {
let old = bundle("a\ncontext\na");
let new = bundle("bbb\ncontext\nbbb");
let exact = old.entry.len() + old.files[&old.entry].len() + new.files[&new.entry].len();
let limits = Limits {
files: 1,
changed_lines: 2,
display_bytes: exact,
};
assert!(Preview::with_limits(&old, &new, limits).is_ok());
assert_eq!(
Preview::with_limits(&old, &new, Limits { files: 0, ..limits }).unwrap_err(),
PreviewError::TooManyFiles { limit: 0 }
);
assert_eq!(
Preview::with_limits(
&old,
&new,
Limits {
changed_lines: 1,
..limits
}
)
.unwrap_err(),
PreviewError::TooManyChangedLines { limit: 1 }
);
assert_eq!(
Preview::with_limits(
&old,
&new,
Limits {
display_bytes: exact - 1,
..limits
}
)
.unwrap_err(),
PreviewError::TooManyDisplayBytes { limit: exact - 1 }
);
let long = bundle(&"a".repeat(LIMITS.display_bytes));
assert!(matches!(
Preview::build(&bundle("b"), &long),
Err(PreviewError::TooManyDisplayBytes { .. })
));
}
#[test]
fn limits_apply_to_the_whole_preview_not_each_file() {
let mut old = bundle("a");
let mut new = bundle("b");
old.files.insert("second.arcadia".into(), "a".into());
new.files.insert("second.arcadia".into(), "b".into());
let limits = Limits {
files: 2,
changed_lines: 2,
display_bytes: old.entry.len() + "second.arcadia".len() + 4,
};
let complete = Preview::with_limits(&old, &new, limits).unwrap();
assert_eq!(complete.files().len(), 2);
assert_eq!(complete.changed_line_count(), 2);
assert_eq!(complete.display_bytes(), limits.display_bytes);
assert!(matches!(
Preview::with_limits(&old, &new, Limits { files: 1, ..limits }),
Err(PreviewError::TooManyFiles { limit: 1 })
));
assert!(matches!(
Preview::with_limits(
&old,
&new,
Limits {
changed_lines: 1,
..limits
}
),
Err(PreviewError::TooManyChangedLines { limit: 1 })
));
assert!(matches!(
Preview::with_limits(
&old,
&new,
Limits {
display_bytes: limits.display_bytes - 1,
..limits
}
),
Err(PreviewError::TooManyDisplayBytes { .. })
));
}
}
+33 -7
View File
@@ -353,6 +353,13 @@ pub fn view(app: &App) -> View<'_, Message> {
if app.pending.is_some() {
return modal(root,column![text("Keep your changes?").size(21),text("Save the model and pending edits before continuing. Invalid drafts stay available for correction.").size(13),row![action("Cancel",Message::Cancel),action("Discard changes",Message::Discard),button("Save & continue").padding(10).on_press(Message::SaveAndContinue)].spacing(8)].spacing(16).into());
}
if app.source_rename.open() {
return modal_width(
root,
crate::app::source_rename::dialog(app),
(app.window_size.width - 32.).clamp(120., 760.),
);
}
root
}
fn modal<'a>(base: View<'a, Message>, content: View<'a, Message>) -> View<'a, Message> {
@@ -1017,6 +1024,13 @@ fn source_files(app: &App) -> View<'_, Message> {
fn source(app: &App) -> View<'_, Message> {
use crate::source_navigation::Event as SourceEvent;
let (undo_label, undo_message) = match app.source_rename_undo_message() {
Some(message) => ("Undo rename", Some(message)),
None => (
"Undo model",
(app.document.can_undo() && !app.source_dirty()).then_some(Message::Undo),
),
};
let navigation: View<'_, Message> = if app.source_navigation.open {
column![
row![
@@ -1131,7 +1145,8 @@ fn source(app: &App) -> View<'_, Message> {
.on_action(Message::SourceEdit)
.key_binding({
let context = app.source_symbol_context();
move |press| source_binding(press, &context)
let rename = app.source_rename_begin();
move |press| source_binding(press, &context, Some(&rename))
})
.font(Font::MONOSPACE)
.size(14)
@@ -1159,8 +1174,8 @@ fn source(app: &App) -> View<'_, Message> {
row![
action("Undo text", Message::SourceUndo),
action("Redo text", Message::SourceRedo),
button(text("Undo model").size(12)).padding([8,10]).style(style::nav)
.on_press_maybe((app.document.can_undo() && !app.source_dirty()).then_some(Message::Undo)),
button(text(undo_label).size(12)).padding([8,10]).style(style::nav)
.on_press_maybe(undo_message),
button(text("Redo model").size(12)).padding([8,10]).style(style::nav)
.on_press_maybe((app.document.can_redo() && !app.source_dirty()).then_some(Message::Redo)),
action("Find / go to…", Message::SourceNavigation(SourceEvent::Open(false))),
@@ -1187,6 +1202,7 @@ fn source(app: &App) -> View<'_, Message> {
row![
action("Definition · F12", app.source_symbol_message(crate::source_navigation::SymbolEvent::Query(crate::source_navigation::query::Action::Definition))),
action("References · Shift F12", app.source_symbol_message(crate::source_navigation::SymbolEvent::Query(crate::source_navigation::query::Action::References))),
action("Rename alias · F2", app.source_rename_message()),
action("Show in diagrams…", app.source_symbol_message(crate::source_navigation::SymbolEvent::DiagramUsages)),
button(text("Back · Alt ←").size(12)).padding([8, 10]).style(style::nav)
.on_press_maybe((app.source_symbols.back.iter().any(|entry| entry.session == app.source_workspace.session())).then(|| app.source_symbol_message(crate::source_navigation::SymbolEvent::Back))),
@@ -1239,13 +1255,23 @@ fn source_layout<'a, Renderer: iced::advanced::text::Renderer + 'a>(
.height(Fill)
.into()
}
fn source_binding(
pub(crate) fn source_binding(
press: widget::text_editor::KeyPress,
context: &crate::source_navigation::SymbolContext,
rename: Option<&crate::app::source_rename::Begin>,
) -> Option<widget::text_editor::Binding<Message>> {
use crate::source_navigation::{SymbolEvent, query::Action};
use iced::keyboard::{Key, key::Named};
use widget::text_editor::{Binding, Status};
if matches!(press.status, Status::Focused { .. })
&& press.modifiers.is_empty()
&& press.key == Key::Named(Named::F2)
&& let Some(begin) = rename
{
return Some(Binding::Custom(Message::SourceRename(
crate::app::source_rename::Event::Begin(begin.clone()),
)));
}
// Windows owns Alt+Space for its window menu. Iced's default binding would
// otherwise insert a supplied space payload, even after that menu closes.
// Keep Ctrl+Alt (AltGr), shifted input, and ordinary Space unchanged.
@@ -1287,7 +1313,7 @@ fn source_binding(
}
#[cfg(all(test, debug_assertions))]
mod source_tests {
pub(crate) mod source_tests {
use super::*;
use iced::advanced::{
Layout, Shell, graphics, layout, renderer, text as shaped, widget as core_widget,
@@ -1296,7 +1322,7 @@ mod source_tests {
use iced::{Background, Color, Pixels, Point, Rectangle, Size, Transformation, Vector};
// Measure real Iced paragraphs/editor text without constructing a window or GPU.
struct FontLayout;
pub(crate) struct FontLayout;
impl renderer::Renderer for FontLayout {
fn start_layer(&mut self, _: Rectangle) {}
fn end_layer(&mut self) {}
@@ -1626,7 +1652,7 @@ mod source_tests {
.size(14)
.padding(16)
.height(Fill)
.key_binding(|press| source_binding(press, &context))
.key_binding(|press| source_binding(press, &context, None))
.into();
let mut tree = core_widget::Tree::new(editor.as_widget());
let size = Size::new(600., 240.);
+39
View File
@@ -2682,6 +2682,45 @@ an Allocation or state-action relation retains that relation as the model action
target. Camera movement preserves a valid marker; changing the selected record,
document, accepted revision or represented context retires it. Navigation does
not alter allocation ownership or the meaning of Delete.</p>
<p><strong>Rename alias</strong> (<span class="docutils literal">F2</span> while the source editor has focus) changes the alias
under the caret or its exact selection. It requires a unique authored identity
in the complete accepted source bundle, with no pending source, property or
behavior drafts and no extra loaded detached files. Accepted but unsaved source
is eligible. The operation MUST NOT implicitly apply a draft or allocate an ID.
The engineering name and UUID remain unchanged.</p>
<p>The <strong>Source alias</strong> field does not trim or normalize the entered alias. Pasting
line breaks, tabs or other control characters is explicitly rejected as a whole:
the field and its selection remain unchanged, and any previous preview approval
is invalidated. <strong>Preview</strong>
prepares a typed alias change and displays every changed file and source line;
comments, strings, engineering data, file membership and declaration ownership
remain unchanged. Enter in the field requests Preview, never confirmation.
The preview is for this operation against accepted source, so a later Save may
also contain earlier accepted edits. A changing rename requires enough model
history capacity to retain its immediate Undo. An invalid, identical, stale or
oversized candidate cannot enable confirmation.</p>
<p>The preview MUST provide access to all admitted files, hunks, lines and long-line
segments. Refuse a candidate whose complete preview exceeds the editor's bounds:
256 changed files, 50,000 changed lines or 8 MiB of displayed source bytes
(both versions of changed/context lines and file paths). Index metadata is
separately bounded to seven retained line records per changed line. These
limits do not restrict the language itself. A changed-line count is not a count
of semantic references. Cancel, Preview and Rename alias remain reachable
outside the independently scrolling source preview.</p>
<p>Explicit <strong>Rename alias</strong> confirms the current preview once, as one accepted
model/source transaction. A source, document, recovery or replacement change
invalidates its approval even if later bytes match the old preview. Closing the
form cancels approval; a late worker result MUST NOT reopen it. Rejected,
canceled and unchanged operations preserve accepted bytes, drafts, save state,
revisions, selection and both histories. Successful rename selects the new alias
at the original occurrence rather than the first matching spelling in the file.
Editing the replacement permits a new Preview in the same form. After a source
context change, close the form and invoke Rename again on the current alias.</p>
<p><strong>Undo rename</strong> is guarded against intervening edits and uses model history;
the persistent <strong>Undo model</strong> and <strong>Redo model</strong> controls restore the complete
accepted snapshots. Text <span class="docutils literal">Ctrl+Z</span> remains <strong>Undo text</strong>. Save is separate and
uses the ordinary guarded source-save contract. An unconfirmed preview and Undo
history are not persisted on reopening.</p>
<p>Changing a graphical UUID must atomically update map keys and every reference.
Alias changes preserve UUID identity. Deletion must account for owned elements,
incident relationships, ordered occurrences, and diagram placements without
+44
View File
@@ -2109,6 +2109,50 @@ target. Camera movement preserves a valid marker; changing the selected record,
document, accepted revision or represented context retires it. Navigation does
not alter allocation ownership or the meaning of Delete.
**Rename alias** (``F2`` while the source editor has focus) changes the alias
under the caret or its exact selection. It requires a unique authored identity
in the complete accepted source bundle, with no pending source, property or
behavior drafts and no extra loaded detached files. Accepted but unsaved source
is eligible. The operation MUST NOT implicitly apply a draft or allocate an ID.
The engineering name and UUID remain unchanged.
The **Source alias** field does not trim or normalize the entered alias. Pasting
line breaks, tabs or other control characters is explicitly rejected as a whole:
the field and its selection remain unchanged, and any previous preview approval
is invalidated. **Preview**
prepares a typed alias change and displays every changed file and source line;
comments, strings, engineering data, file membership and declaration ownership
remain unchanged. Enter in the field requests Preview, never confirmation.
The preview is for this operation against accepted source, so a later Save may
also contain earlier accepted edits. A changing rename requires enough model
history capacity to retain its immediate Undo. An invalid, identical, stale or
oversized candidate cannot enable confirmation.
The preview MUST provide access to all admitted files, hunks, lines and long-line
segments. Refuse a candidate whose complete preview exceeds the editor's bounds:
256 changed files, 50,000 changed lines or 8 MiB of displayed source bytes
(both versions of changed/context lines and file paths). Index metadata is
separately bounded to seven retained line records per changed line. These
limits do not restrict the language itself. A changed-line count is not a count
of semantic references. Cancel, Preview and Rename alias remain reachable
outside the independently scrolling source preview.
Explicit **Rename alias** confirms the current preview once, as one accepted
model/source transaction. A source, document, recovery or replacement change
invalidates its approval even if later bytes match the old preview. Closing the
form cancels approval; a late worker result MUST NOT reopen it. Rejected,
canceled and unchanged operations preserve accepted bytes, drafts, save state,
revisions, selection and both histories. Successful rename selects the new alias
at the original occurrence rather than the first matching spelling in the file.
Editing the replacement permits a new Preview in the same form. After a source
context change, close the form and invoke Rename again on the current alias.
**Undo rename** is guarded against intervening edits and uses model history;
the persistent **Undo model** and **Redo model** controls restore the complete
accepted snapshots. Text ``Ctrl+Z`` remains **Undo text**. Save is separate and
uses the ordinary guarded source-save contract. An unconfirmed preview and Undo
history are not persisted on reopening.
Changing a graphical UUID must atomically update map keys and every reference.
Alias changes preserve UUID identity. Deletion must account for owned elements,
incident relationships, ordered occurrences, and diagram placements without
@@ -0,0 +1,2 @@
correction-tests.log -text
correction-tests-receipt.json -text
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,7 @@
# Diagram navigation coverage corrections — handoff, 2026-09-12
**Root verification update:** Windows desktop execution passed: 894 passed, 0 failed, 1 ignored on source46b4f3c. See the [complete result and input evidence](correction-tests.md). Core-specific checks, strict lint, matching GUI and native acceptance remain separate.
## Candidate and ownership
Implemented on branch `codex/diagram-coverage-fixes-ytr83r-20260912`. Production base is `fdf756e6a41b1879760e45c2c263bd671b562fe3`; the root's subsequent docs-only commit does not change that production base. Candidate HEAD when this pre-format manifest was captured: `f23615ffebdc3ca6872f98b6889ec49bb4e8abda`.
@@ -0,0 +1,8 @@
{
"command": "cargo test -p oa-desktop --all-targets --locked --offline",
"startUtc": "2026-09-12T22:10:52.9461475Z",
"endUtc": "2026-09-12T22:42:39.0355299Z",
"base": "46b4f3c0d0f04050ad6d705051ee0199138e47a2",
"uncommittedChanges": true,
"exitCode": 0
}
@@ -0,0 +1,910 @@
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\diagram-coverage-fixes-ytr83r-20260912\crates\oa-core)
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\diagram-coverage-fixes-ytr83r-20260912\crates\oa-server)
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\diagram-coverage-fixes-ytr83r-20260912\crates\oa-desktop)
Finished `test` profile [unoptimized] target(s) in 12m 44s
Running unittests src\main.rs (C:\Users\User\Documents\Git\ARCADIA-Studio\target\debug\deps\arcadia_studio-27db9628b266bf39.exe)
running 895 tests
test app::architecture_authoring::tests::architecture_blank_names_and_escape_never_accept_placeholder_or_rename ... ok
test app::architecture_authoring::tests::architecture_captured_escape_does_not_cancel_other_controls_or_placement ... ok
test app::architecture_authoring::tests::architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo ... ok
test app::architecture_authoring::tests::architecture_allocated_work_is_root_owned_and_one_exact_history_change ... ok
test app::architecture_authoring::tests::architecture_invalid_click_cannot_poison_inline_or_checkpoint_geometry ... ok
test app::architecture_authoring::tests::architecture_first_focused_input_escape_cancels_create_and_rename ... ok
test app::architecture_authoring_tests::architecture_cancelled_gesture_cannot_commit_a_later_named_draft ... ok
test app::architecture_authoring::tests::architecture_stale_submit_keeps_create_and_rename_visible_and_exact ... ok
test app::architecture_authoring_tests::architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan ... ok
test app::architecture_authoring_tests::architecture_checkpoint_retains_exact_name_point_and_fresh_binding_without_commands ... ok
test app::architecture_authoring_tests::architecture_context_choices_are_small_and_system_is_singleton ... ok
test app::architecture_authoring_tests::architecture_creation_names_before_commit_preserves_camera_and_undoes_once ... ok
test app::architecture_authoring_tests::architecture_contextual_allocation_creates_work_relation_and_node_in_one_undo ... ok
test app::architecture_authoring_tests::architecture_existing_inventory_is_contextual_and_placement_never_implies_allocation ... ok
test app::architecture_authoring_tests::architecture_name_survives_rejected_click_resize_navigation_and_escape_is_explicit ... ok
test app::architecture_authoring_tests::architecture_f2_rename_preserves_identity_placement_and_atomic_history ... ok
test app::architecture_authoring_tests::basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture ... ok
test app::architecture_authoring_tests::architecture_rejected_submission_keeps_inline_name_visible_and_invalid_click_is_ignored ... ok
test app::behavior_lifecycle_tests::passive_behavior_canvas_events_do_not_apply_property_drafts ... ok
test app::behavior_lifecycle_tests::incomplete_behavior_form_is_dirty_close_save_retains_it_and_cancel_allows_switching ... ok
test app::behavior_lifecycle_tests::pending_behavior_blocks_navigation_property_edits_and_model_history ... ok
test app::behavior_lifecycle_tests::pending_behavior_blocks_source_creation_and_source_history_replay ... ok
test app::behavior_lifecycle_tests::save_prepares_behavior_before_opening_destination_and_cancel_keeps_one_undo ... ok
test app::behavior_lifecycle_tests::source_writeback_failure_keeps_typed_behavior_form_for_repair_and_one_undo ... ok
test app::checkpoints::tests::checkpoint_completion_releases_the_writer_while_a_context_menu_is_open ... ok
test app::checkpoints::tests::checkpoint_inline_create_and_rename_restore_focus_without_changing_drafts_or_history ... ok
test app::checkpoints::tests::checkpoint_noninline_contexts_keep_other_focus_and_still_lint ... ok
test app::checkpoints::tests::discard_and_close_waits_for_the_late_checkpoint_then_retires_it ... ok
test app::checkpoints::tests::home_resume_keeps_state_view_while_explicit_architecture_remains_reachable ... ok
test app::checkpoints::tests::home_resume_returns_to_each_actual_workspace_without_model_edits ... ok
test app::checkpoints::tests::home_resume_retains_invalid_source_and_property_drafts ... ok
test app::checkpoints::tests::malformed_checkpoint_does_not_replace_the_current_model ... ok
test app::checkpoints::tests::recent_and_recovery_navigation_respect_unsaved_decision ... ok
test app::checkpoints::tests::restored_copy_keeps_raw_property_and_requires_save_without_local_or_hosted_binding ... ok
test app::checkpoints::tests::stale_write_completion_cannot_recover_a_discarded_document ... ok
test app::checkpoints::tests::walkthrough_request_keeps_unfinished_source_behind_the_normal_pending_guard ... ok
test app::data_paging_lifecycle_tests::paging_preserves_same_context_and_resets_on_layer_or_document_replacement ... ok
test app::diagram_reveal::tests::actual_window_resize_before_first_measurement_retains_focus_for_the_new_context ... ok
test app::diagram_reveal::tests::all_reveal_surfaces_recenter_once_after_the_real_viewport_and_preserve_authored_state ... ok
test app::checkpoints::tests::saving_an_older_snapshot_keeps_recovery_for_newer_unsaved_work ... ok
test app::diagram_reveal::tests::malformed_or_wrong_stamp_measurements_cannot_poison_a_pending_reveal ... ok
test app::diagram_selection_tests::changing_visible_view_follows_its_clean_inspector_without_a_model_edit ... ok
test app::diagram_reveal::tests::user_camera_edit_and_navigation_cancel_deferred_focus_without_late_snapping ... ok
test app::diagram_selection_tests::diagram_switch_preserves_an_independent_semantic_inspection ... ok
test app::diagram_selection_tests::diagram_uuid_edit_follows_the_same_view_or_a_different_picker_target ... ok
test app::diagram_selection_tests::invalid_view_draft_blocks_the_switch_and_retains_raw_input ... ok
test app::diagram_selection_tests::stale_diagram_choice_never_applies_an_unrelated_valid_draft ... ok
test app::diagram_selection_tests::valid_pending_view_edit_applies_to_old_view_then_inspects_the_new_view ... ok
test app::diagram_usages::tests::accepted_new_chooser_retires_old_measurement_without_blocking_future_viewports ... ok
test app::diagram_usages::tests::allocation_navigation_keeps_relation_selection_and_exact_visual_work_lifetime ... ok
test app::diagram_usages::tests::asynchronous_lookup_is_loading_inert_and_retains_filter_until_completion ... ok
test app::diagram_usages::tests::closed_lookup_keeps_slot_and_replacement_waits_for_exact_old_completion ... ok
test app::diagram_usages::tests::caret_uses_exact_verified_alias_identity_and_preserves_source_cursor ... ok
test app::diagram_usages::tests::completion_releases_worker_behind_busy_modal_without_navigation ... ok
test app::diagram_usages::tests::completion_after_cancel_releases_slot_without_reopening_or_moving ... ok
test app::diagram_usages::tests::forged_completion_cannot_release_another_slot_or_admit_wrong_context ... ok
test app::diagram_usages::tests::dirty_or_comment_caret_has_no_selected_object_fallback ... ok
test app::diagram_usages::tests::old_panel_tokens_and_forged_targets_cannot_redirect_selection ... ok
test app::diagram_usages::tests::oversized_node_focus_stays_on_label_and_single_surface_query_excludes_other_views ... ok
test app::diagram_usages::tests::port_navigation_selects_port_and_exact_visible_owner_without_editing_geometry ... ok
test app::diagram_usages::tests::query_presentation_snapshot_keeps_expansion_without_retaining_live_state ... ok
test app::diagram_usages::tests::pending_source_behavior_and_modal_inputs_block_open_and_choose_without_loss ... ok
test app::diagram_usages::tests::repeated_preview_arrows_select_the_relation_and_highlight_the_exact_caller_link ... ok
test app::diagram_usages::tests::repeated_scenario_messages_select_the_exact_occurrence_and_move_camera ... ok
test app::diagram_usages::tests::repeated_process_calls_reveal_the_chosen_caller_and_distinct_tile_camera ... ok
test app::diagram_usages::tests::revision_retarget_and_document_change_make_old_destinations_inert ... ok
test app::diagram_usages::tests::reset_keeps_old_worker_slot_and_only_new_document_results_are_admitted ... ok
test app::diagram_usages::tests::saved_node_and_edge_navigation_preserve_whole_sources_and_actual_history ... ok
test app::diagram_usages::tests::stale_caret_completion_never_opens_diagrams_for_current_selection ... ok
test app::diagram_usages::tests::stale_open_context_cannot_replace_a_panel ... ok
test app::diagram_usages::tests::stale_revision_completion_refreshes_asynchronously_without_activating ... ok
test app::diagram_usages::tests::state_row_navigation_has_exact_visual_row_without_retargeting_the_selected_relation ... ok
test app::diagram_usages::tests::tree_origin_captures_subject_and_never_falls_back_to_current_selection_or_name ... ok
test app::diagram_usages::tests::whole_surface_camera_fits_huge_extent_while_target_camera_remains_readable ... ok
test app::diagram_usages::tests::worker_failure_finishes_loading_and_can_be_retried_without_losing_sources ... ok
test app::direct_scenario_tests::direct_scenario_camera_details_and_real_resize_invalidate_only_pointer_context ... ok
test app::direct_scenario_tests::direct_scenario_rejects_every_stale_context_and_preserves_property_drafts ... ok
test app::direct_scenario_tests::direct_scenario_creation_and_reorder_preserve_selection_and_each_undo_once ... ok
test app::direct_scenario_tests::direct_scenario_source_failure_and_navigation_keep_form_until_success ... ok
test app::hosted::tests::hosted_catalog_invalidation_follows_service_writes_and_observed_revision_changes ... ok
test app::hosted::tests::hosted_catalog_search_credentials_and_failed_refresh_never_mix_result_scopes ... ok
test app::hosted::tests::hosted_changed_bound_head_does_not_invalidate_another_services_empty_filter ... ok
test app::hosted::tests::hosted_conflict_fetches_review_without_adoption_and_reload_is_undoable ... ok
test app::hosted::tests::hosted_detached_drafts_and_recovery_remain_available_after_rejected_publish ... ok
test app::hosted::tests::hosted_failed_cross_service_publication_checks_and_reloads_its_own_resource ... ok
test app::hosted::tests::hosted_failed_or_cancelled_local_open_keeps_the_existing_document_and_destination ... ok
test app::hosted::tests::hosted_invalid_and_detached_source_drafts_never_start_network_writes ... ok
test app::hosted::tests::hosted_actual_app_tasks_publish_edit_conflict_correct_credentials_reload_and_open ... ok
test app::hosted::tests::hosted_modal_blocks_underlying_edits_and_credentials_are_redacted ... ok
test app::hosted::tests::hosted_new_revision_invalidates_filtered_catalog_even_without_a_displayed_row ... ok
test app::hosted::tests::hosted_only_successful_source_save_as_changes_the_active_destination ... ok
test app::hosted::tests::hosted_open_waits_for_unsaved_guard_and_failed_open_preserves_current_document ... ok
test app::hosted::tests::hosted_publish_acknowledges_only_the_sent_snapshot_and_retains_undo ... ok
test app::hosted::tests::hosted_invalid_local_open_repair_saves_locally_and_never_replaces_old_resource ... ok
test app::hosted::tests::hosted_stale_completions_do_not_clear_current_busy_or_adopt_another_document ... ok
test app::hosted::tests::hosted_unknown_save_is_acknowledged_by_exact_readback_without_retry ... ok
test app::lint_tests::committed_check_cache_follows_edit_and_undo_while_drafts_stay_separate ... ok
test app::lint_tests::lint_coalesces_work_and_rejects_obsolete_results ... ok
test app::lint_tests::prior_document_lint_cannot_enter_reopened_model_or_block_next_checks ... ok
test app::port_view_tests::catalog_creation_follows_the_new_port_and_keeps_group_browsing_explicit ... ok
test app::port_view_tests::filtered_active_view_cannot_hide_a_created_port_when_eligible_owner_view_is_folded ... ok
test app::process_canvas::direct_process_dirty_source_blocks_canvas_creation_without_losing_text ... ok
test app::process_canvas::tests::direct_process_app_open_source_and_back_preserve_local_reference_identity ... ok
test app::process_canvas::tests::direct_process_app_same_owner_selection_preserves_tool_but_owner_change_resets_it ... ok
test app::process_canvas::tests::direct_process_app_save_details_resize_and_passive_geometry_keep_drafts_and_camera ... ok
test app::process_canvas::tests::direct_process_app_source_failure_retains_form_then_commits_once_and_selects_occurrence ... ok
test app::process_canvas::tests::direct_process_app_stale_context_dirty_source_and_properties_never_mutate ... ok
test app::process_canvas::tests::process_focus_retains_rejected_draft_and_guards_selection_until_panels_restore ... ok
test app::process_canvas::tests::direct_process_append_empty_first_second_exchange_has_exact_source_history ... ok
test app::reference_authoring::tests::inspector_reference_browse_is_read_only_then_one_scoped_value_applies_with_exact_undo_redo ... ok
test app::reference_authoring::tests::inspector_reference_retains_invalid_missing_and_incompatible_current_until_explicit_clear ... ok
test app::reference_authoring::tests::chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_history_text_out ... ok
test app::reference_authoring::tests::mounted::mounted_form_tab_enters_in_both_directions_and_cycles_boundaries_without_losing_focus ... ok
test app::reference_authoring::tests::mounted::mounted_delayed_return_cannot_focus_under_review_and_fresh_modal_tab_owns_its_form ... ok
test app::reference_authoring::tests::mounted::mounted_reference_choice_stages_literal_identity_and_delayed_focus_cannot_hit_replacement_field ... ok
test app::reference_authoring::tests::mounted::mounted_fresh_tab_reveals_offscreen_reference_and_return_reveals_it_again ... ok
test app::reference_authoring::tests::mounted::mounted_reverse_review_required_rationale_remains_editable_beside_candidate_reference_form ... ok
test app::reference_authoring::tests::mounted::mounted_reference_trigger_tabs_to_root_popup_search_escape_and_exact_adjacent_fields ... ok
test app::reference_authoring::tests::mounted::mounted_scrolled_reference_anchor_uses_window_geometry_for_pointer_and_keyboard ... ok
test app::reference_authoring::tests::reference_array_row_reorder_and_constructor_switch_invalidate_exact_old_field ... ok
test app::reference_authoring::tests::review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_focus ... ok
test app::reference_authoring::tests::review_reference_facets_groups_and_attachment_hosts_browse_without_replanning_and_reject_old_facet ... ok
test app::repair_save_state_tests::recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model ... ok
test app::repair_save_state_tests::recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority ... ok
test app::repair_save_state_tests::repair_history_compares_to_new_disk_baseline_and_then_successful_save ... ok
test app::repair_save_state_tests::repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close ... ok
test app::repair_save_state_tests::valid_equal_content_at_distinct_destinations_stays_clean ... ok
test app::review_navigation_tests::rejected_semantic_source_writeback_retains_review_camera_and_original_navigation ... ok
test app::review_navigation_tests::review_already_showing_the_target_keeps_its_camera_and_query ... ok
test app::review_navigation_tests::semantic_name_take_selects_the_counterpart_view_in_both_directions_with_one_exact_history_entry ... ok
test app::review_navigation_tests::semantic_review_without_a_destination_view_clears_the_old_layer_canvas_without_creating_a_view ... ok
test app::save_as_tests::save_as_late_receipt_preserves_newer_raw_source_draft_and_destination ... ok
test app::save_as_tests::save_as_preflight_rejects_historical_collision_before_starting_io ... ok
test app::save_as_tests::save_as_receipt_then_one_undo_restores_meaning_and_saving_preserves_redo ... ok
test app::source_project_tests::closed_detached_file_keeps_first_save_conflict_then_can_be_reloaded_fresh ... ok
test app::source_project_tests::inactive_invalid_draft_blocks_graphical_changes_and_diagnostic_switches_files ... ok
test app::source_project_tests::json_export_and_rejected_save_as_preserve_source_location_history_and_bytes ... ok
test app::source_project_tests::missing_include_open_keeps_old_model_and_can_be_repaired_and_saved ... ok
test app::source_project_tests::recovery_after_first_save_failure_retains_target_then_retries_without_reentering_path ... ok
test app::source_project_tests::recovery_of_previous_save_as_target_does_not_redirect_current_project ... ok
test app::source_project_tests::recovery_open_finds_first_save_journal_without_an_entry_file ... ok
test app::source_project_tests::recovery_target_survives_edit_lint_apply_and_rejected_recovery_until_success ... ok
test app::source_project_tests::source3_native_open_preserves_report_comments_nesting_and_save_reopen ... ok
test app::source_project_tests::two_file_apply_graphical_edit_exact_undo_save_as_and_reopen ... ok
test app::source_symbols::tests::back_after_origin_edit_uses_its_current_cursor_instead_of_the_old_range ... ok
test app::source_symbols::tests::back_keeps_the_latest_64_actual_locations_without_reusing_dropped_history ... ok
test app::source_symbols::tests::back_restores_exact_reversed_utf8_selection_and_crlf_source ... ok
test app::source_symbols::tests::captured_find_and_modal_escape_use_one_actual_subscription_and_preserve_pending_work ... ok
test app::source_symbols::tests::captured_subscription_does_not_duplicate_existing_behavior_or_architecture_routes ... ok
test app::source_symbols::tests::close_during_busy_hides_panel_and_completion_does_not_reopen_it ... ok
test app::source_symbols::tests::closed_results_stay_closed_on_exact_back_and_reopen_only_for_a_return_notice ... ok
test app::source_symbols::tests::combined_modal_completion_releases_worker_without_navigation_or_model_changes ... ok
test app::source_symbols::tests::combined_reference_chooser_blocks_navigation_and_completion_precedes_reconciliation ... ok
test app::source_symbols::tests::completion_rejects_file_edit_session_cursor_interaction_and_raw_input_changes ... ok
test app::source_symbols::tests::detached_duplicate_requires_an_explicit_choice_even_when_included_source_is_validated ... ok
test app::source_symbols::tests::escape_dismisses_modal_before_underlying_source_results ... ok
test app::source_symbols::tests::escape_dismisses_results_and_cancels_busy_navigation_without_changing_sources ... ok
test app::source_symbols::tests::find_close_releases_removed_input_and_guarded_focus_respects_later_input ... ok
test app::source_symbols::tests::find_only_captured_escape_closes_once_and_preserves_raw_query_and_selection ... ok
test app::source_symbols::tests::invalid_unapplied_drafts_navigate_without_apply_or_losing_either_history ... ok
test app::source_symbols::tests::matching_reveal_ticket_is_required_and_raw_input_invalidates_pending_reveal ... ok
test app::source_symbols::tests::old_find_close_cannot_cancel_a_new_file_session_or_reopened_find_intent ... ok
test app::source_symbols::tests::old_symbol_widget_envelopes_after_file_switch_preserve_same_session_intent ... ok
test app::source_symbols::tests::old_symbol_widget_envelopes_after_reset_preserve_new_same_path_intent ... ok
test app::source_symbols::tests::reset_retains_worker_slot_until_old_completion_then_admits_new_session_query ... ok
test app::source_symbols::tests::saturated_raw_clock_never_authorizes_completion_or_reveal ... ok
test app::source_symbols::tests::source_symbols_100_modules_1000_definitions_10000_uses ... ignored, manual 100-module latency/process-memory trial; run alone with --ignored --nocapture --test-threads=1
test app::source_symbols::tests::unknown_job_cannot_release_current_worker_and_old_duplicate_cannot_replace_new_results ... ok
test app::source_symbols::tests::valid_definition_and_reference_choice_keep_whole_model_and_source_unchanged ... ok
test app::state_diagram_tests::all_context_dimensions_reject_stale_selection_camera_and_model_messages ... ok
test app::state_diagram_tests::create_view_places_only_its_machine_and_restores_exact_source_in_one_undo ... ok
test app::state_diagram_tests::direct_create_connect_select_and_move_keep_semantic_identity_and_atomic_history ... ok
test app::state_diagram_tests::fallback_owner_first_selection_keeps_the_captured_context_for_its_first_move ... ok
test app::state_diagram_tests::pending_drafts_block_every_direct_mutation_and_passive_camera_preserves_inputs ... ok
test app::reference_authoring::tests::mounted::mounted_review_pointer_open_and_outside_dismissal_keep_background_input_inactive ... ok
test app::state_diagram_tests::rejected_creation_keeps_source_and_selection_then_valid_owner_can_retry ... ok
test app::state_diagram_tests::real_view_tool_and_history_changes_invalidate_old_release_events ... ok
test app::state_diagram_tests::same_size_viewport_after_revision_or_tool_epoch_does_not_refit_the_camera ... ok
test app::state_diagram_tests::shared_semantic_states_keep_architecture_and_behavior_placements_independent ... ok
test app::state_diagram_tests::state_diagram_routes_constructor_and_source_open_to_the_existing_workspace_without_edits ... ok
test app::state_diagram_tests::state_diagram_routes_picker_and_reveal_but_explicit_architecture_does_not_bounce ... ok
test app::state_diagram_tests::state_navigation_catalog_selection_across_layers_resets_camera_and_tool ... ok
test app::state_diagram_tests::state_diagram_routes_preserve_selected_raw_properties_and_pending_behavior_guards ... ok
test app::state_diagram_tests::state_navigation_layer_change_resets_the_new_machine_at_the_same_viewport_size ... ok
test app::state_diagram_tests::state_navigation_catalog_selection_of_another_machine_resets_camera_and_tool ... ok
test app::state_diagram_tests::state_navigation_root_event_and_same_layer_keep_the_current_camera_and_tool ... ok
test app::state_diagram_tests::state_navigation_return_to_the_same_view_preserves_camera_tool_and_fit_mode ... ok
test app::transition_context_tests::explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture ... ok
test app::transition_context_tests::explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view ... ok
test app::transition_context_tests::rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start ... ok
test app::transition_context_tests::machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera ... ok
test app::view_copy_tests::copying_architecture_keeps_architecture_even_with_a_retained_state_view ... ok
test app::transition_context_tests::scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions ... ok
test app::view_copy_tests::copying_the_current_state_view_uses_its_own_identity_and_is_one_source_undo ... ok
test app::view_copy_tests::duplicate_rejection_preserves_an_incomplete_source_buffer ... ok
test app::view_copy_tests::stale_copy_does_not_apply_an_unrelated_valid_property_draft ... ok
test app::view_copy_tests::duplicate_rejection_preserves_invalid_property_and_pending_behavior_drafts ... ok
test behavior::canvas_workspace::tests::relative_camera_zoom_keeps_the_cursor_world_point_and_rejects_nonfinite_inputs ... ok
test behavior::canvas_workspace::tests::viewport_ordered_batch_uses_each_event_position_and_cancels_before_camera_motion ... ok
test behavior::contextual_source_tests::contextual_source_app_checkpoint_restores_inner_raw_draft_without_model_adoption ... ok
test behavior::contextual_source_tests::contextual_source_app_empty_actions_and_child_open_refuse_other_raw_editors ... ok
test behavior::contextual_source_tests::contextual_source_app_modal_escape_then_child_back_then_parent_cancel_preserve_accepted_source ... ok
test behavior::contextual_source_tests::contextual_source_back_retains_parent_and_rejects_late_child_event ... ok
test behavior::contextual_source_tests::contextual_source_back_remains_available_after_owner_deletion_and_with_dirty_editors ... ok
test behavior::contextual_source_tests::contextual_source_default_consumer_name_has_stable_alias_and_correlated_selection ... ok
test behavior::contextual_source_tests::contextual_source_deleted_owner_view_retains_draft_instead_of_panicking_or_using_fallback ... ok
test behavior::contextual_source_tests::contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_layer ... ok
test behavior::contextual_source_tests::contextual_source_empty_history_reports_no_change_and_success_requires_a_real_history_step ... ok
test behavior::contextual_source_tests::contextual_source_existing_choice_prunes_staged_dependency ... ok
test behavior::contextual_source_tests::contextual_source_failed_source_budget_preserves_both_drafts_and_accepted_bytes ... ok
test behavior::contextual_source_tests::contextual_source_incomplete_recovered_kind_reports_error_without_panicking ... ok
test behavior::contextual_source_tests::contextual_source_inserts_work_on_the_exact_guarded_link_and_refuses_exchange_identity_conflict ... ok
test behavior::contextual_source_tests::contextual_source_machine_before_any_owner_creates_optional_view_atomically ... ok
test behavior::contextual_source_tests::contextual_source_lifelines_stage_exact_separate_records_and_one_history_entry ... ok
test behavior::contextual_source_tests::contextual_source_mounted_small_editor_keeps_back_and_use_outside_field_scroll ... ok
test behavior::contextual_source_tests::contextual_source_nested_semantic_parent_is_explicit_and_revalidated ... ok
test behavior::contextual_source_tests::contextual_source_new_callee_lifeline_preserves_caller_bindings_as_an_explicit_warning ... ok
test behavior::contextual_source_tests::contextual_source_original_document_remains_bound_after_back_or_choosing_existing ... ok
test behavior::contextual_source_tests::contextual_source_owner_first_can_return_to_an_unnamed_machine_without_losing_source ... ok
test behavior::contextual_source_tests::contextual_source_recovery_keeps_inner_raw_fields_and_refuses_deleted_or_changed_owner ... ok
test behavior::contextual_source_tests::contextual_source_second_system_and_role_machine_owner_refuse_without_fallback ... ok
test behavior::creation::app_tests::app_choosing_another_behavior_or_layer_cannot_commit_a_creation_draft ... ok
test behavior::contextual_source_tests::contextual_source_work_uses_reserved_contribution_and_link_ids_and_one_undo ... ok
test behavior::creation::app_tests::app_creation_opens_the_actual_new_owner_instead_of_first_or_previously_active_behavior ... ok
test behavior::creation::app_tests::app_first_captured_escape_dismisses_file_or_pending_overlay_before_creation ... ok
test behavior::creation::app_tests::app_lesson_request_cancel_retains_creation_then_escape_discards_only_the_draft ... ok
test behavior::creation::app_tests::app_machine_behavior_submit_opens_its_opted_in_view_as_one_undoable_transaction ... ok
test behavior::creation::app_tests::app_new_never_auto_applies_valid_or_invalid_property_and_source_drafts ... ok
test behavior::creation::app_tests::app_recovered_rootless_sa_creation_retains_layer_ids_and_other_raw_drafts ... ok
test behavior::creation::app_tests::app_stale_layer_open_never_starts_or_replaces_a_draft ... ok
test behavior::creation::tests::all_four_viewpoints_are_explicit_and_survive_source_history_reopen ... ok
test behavior::creation::tests::cancel_and_illegal_family_layer_switches_never_create_model_objects ... ok
test behavior::creation::tests::corrupted_recovery_and_reserved_identity_collision_refuse_without_repairing_raw_values ... ok
test behavior::creation::tests::creation_selection_rejects_changed_scenario_body_and_post_creation_operations ... ok
test behavior::creation::tests::machine_requires_valid_owner_and_first_view_adds_no_region_or_state ... ok
test behavior::creation::tests::mounted_creation_keeps_actions_visible_and_renders_only_six_owner_candidates ... ok
test behavior::creation::tests::oa_process_and_sa_chain_keep_exact_names_root_ownership_and_one_history_entry ... ok
test behavior::creation::tests::optional_context_collapses_without_losing_authored_reference_or_raw_search ... ok
test behavior::creation::tests::owner_search_is_name_ordered_filtered_and_page_sized_not_a_catalog ... ok
test behavior::creation::tests::post_commit_selection_is_correlated_with_exact_created_behavior_and_view ... ok
test behavior::creation::tests::rejected_source_transaction_keeps_draft_and_every_accepted_source_byte ... ok
test behavior::creation::tests::recovery_preserves_exact_raw_inputs_reserved_ids_and_revalidates_stale_owner ... ok
test behavior::creation::tests::repeated_owner_names_remain_distinguishable_by_searchable_parent_context_and_alias ... ok
test behavior::direct_canvas_tests::direct_alt_execution_stops_each_operand_at_its_reply_and_marks_open_operand ... ok
test behavior::direct_canvas_tests::direct_bounds_change_during_held_drag_never_commits_old_slot ... ok
test behavior::direct_canvas_tests::direct_context_escape_unfocus_and_pending_form_cancel_gestures ... ok
test behavior::direct_canvas_tests::direct_clicked_occurrence_capture_cancels_on_view_and_context_changes ... ok
test behavior::direct_canvas_tests::direct_drag_and_click_click_use_ordered_sources_and_publish_once ... ok
test behavior::direct_canvas_tests::direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call ... ok
test behavior::direct_canvas_tests::direct_scroll_resize_and_bounds_changes_cancel_captured_slots ... ok
test behavior::direct_canvas_tests::direct_panned_canvas_maps_ordered_pointer_events_to_the_visible_slot ... ok
test behavior::direct_canvas_tests::direct_selected_handle_reorders_and_hits_keep_semantic_identity ... ok
test behavior::direct_canvas_tests::reused_exchange_messages_display_their_distinct_occurrence_names ... ok
test behavior::scenario::data_contract_tests::recursive_browsing_up_and_root_keep_values_and_report_the_depth_limit ... ok
test behavior::scenario::data_contract_tests::recursive_value_focus_exposes_authored_leaf_and_phase_changes_retain_surviving_keys ... ok
test behavior::scenario::data_contract_tests::large_payload_has_one_editor_with_reachable_field_and_authored_pages ... ok
test behavior::scenario::data_contract_tests::reply_call_picker_previews_the_same_value_reset_as_selecting_another_exchange ... ok
test behavior::scenario::direct_tests::direct_busy_save_keeps_details_current_and_camera_for_the_next_pointer_gesture ... ok
test behavior::scenario::data_contract_tests::scenario_form_commits_two_reused_record_values_and_correlated_reply_atomically ... ok
test behavior::scenario::direct_tests::direct_clicked_occurrence_rejects_stale_slots_and_retains_recoverable_drafts ... ok
test behavior::scenario::direct_tests::direct_exchange_compatibility_browsing_preserves_final_slot_validation ... ok
test behavior::scenario::direct_tests::direct_exchange_picker_is_bounded_and_browsing_keeps_pending_values ... ok
test behavior::scenario::direct_tests::direct_five_hundred_exchange_choice_render_probe ... ok
test behavior::scenario::direct_tests::direct_form_keeps_the_same_canvas_in_the_widget_tree ... ok
test behavior::scenario::direct_tests::direct_middle_pair_insertion_is_one_exact_source_and_model_undo ... ok
test behavior::scenario::direct_tests::direct_operand_pair_keeps_parent_sequence_and_cannot_reorder_across_operands ... ok
test behavior::scenario::direct_tests::direct_clicked_occurrences_use_root_and_operand_slots_with_exact_source_history ... ok
test behavior::scenario::direct_tests::direct_reply_choices_validate_the_actual_slot_and_reorder_rejects_reverse_order ... ok
test behavior::scenario::direct_tests::direct_scenario_reference_navigation_blocks_pending_drafts_and_stale_contexts ... ok
test behavior::scenario::direct_tests::direct_scenario_reference_navigation_retains_each_exact_caller_without_history ... ok
test behavior::scenario::direct_tests::direct_scenario_reference_return_survives_rename_and_rejects_deleted_or_changed_use ... ok
test behavior::scenario::operation_tests::operation_gui_async_picker_uses_request_root_and_nested_directions_without_erasing_rejected_values ... ok
test behavior::scenario::operation_tests::operation_gui_selects_two_parameterless_allocations_without_inventing_refinement_and_reply_derives_call ... ok
test behavior::scenario::operation_tests::operation_gui_unresolved_and_disabled_selection_recovery_preserves_rejected_raw_values ... ok
test behavior::scenario::tests::fragment_operands_accept_paired_messages_through_normal_destination_picker ... ok
test behavior::scenario::tests::graphical_call_reply_is_atomic_correlated_and_source_equivalent ... ok
test behavior::scenario::tests::invalid_lifecycle_edit_keeps_draft_and_timer_has_no_global_exchange ... ok
test behavior::scenario::tests::lifeline_picker_uses_core_viewpoint_contract ... ok
test behavior::scenario::tests::reverse_communication_canvas_choices_and_correlated_reply_share_core_direction ... ok
test behavior::scenario::tests::scenario_reuse_requires_explicit_compatible_bindings ... ok
test behavior::scenario::tests::tagged_scenario_commit_is_not_redirected_by_other_editor_events ... ok
test behavior::scenario::usage::tests::scenario_operation_label_membership_excludes_ellipsis_only_and_normalizes_whitespace ... ok
test behavior::scenario::usage::tests::scenario_usage_counts_nested_occurrences_and_lifelines_but_keeps_reuse_opaque ... ok
test behavior::scenario::usage::tests::scenario_usage_keeps_repeated_messages_and_reveal_is_read_only_and_revalidated ... ok
test behavior::scenario::usage::tests::scenario_usage_only_reports_the_resolved_operation_fields_actually_in_the_label ... ok
test behavior::scenario::workspace::tests::new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session ... ok
test behavior::scenario::workspace::tests::production_empty_scenario_lifeline_tool_keeps_staged_source_inside_stable_canvas ... ok
test behavior::scenario::workspace::tests::production_scenario_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok
test behavior::scenario::workspace::tests::production_scenario_canvas_keeps_bounds_draft_identity_and_camera_through_editing ... ok
test behavior::scenario::workspace::tests::production_scenario_canvas_pan_invalidates_a_pressed_creation_and_caller_restores_once ... ok
test behavior::scenario::direct_tests::direct_reply_choices_cache_keeps_typing_fast_and_revalidates_changed_contexts ... ok
test behavior::scenario::workspace::tests::production_scenario_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok
test behavior::scenario::workspace::tests::production_scenario_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok
test behavior::scenario::workspace::tests::production_scenario_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok
test behavior::scenario::workspace::tests::production_scenario_whole_glyph_drag_moves_one_occurrence_and_rejects_other_operand ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_missing_exact_target_leaves_other_inputs_untouched ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_chained_apply_rechecks_live_review_owner ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_returns_from_back_use_and_escape_after_initial_focus_is_consumed ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_targets_the_name_and_rejects_stale_sessions_or_modals ... ok
test behavior::scenario::workspace::tests::scenario_workspace_choosing_tools_preserves_valid_and_invalid_pending_editors ... ok
test behavior::scenario::workspace::tests::scenario_workspace_fit_uses_measured_inner_viewport_and_rejects_nonfinite_inputs ... ok
test behavior::scenario::workspace::tests::scenario_workspace_open_and_back_restore_owner_and_mounted_camera_after_preselection ... ok
test behavior::scenario::workspace::tests::scenario_workspace_same_size_owner_and_document_changes_remeasure_fit_viewport ... ok
test behavior::scenario::workspace::tests::scenario_workspace_scaled_and_panned_scene_keeps_clicked_slots_and_message_endpoints ... ok
test behavior::scenario::workspace::tests::scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_identity ... ok
test behavior::scenario::workspace::tests::scenario_workspace_viewport_events_are_passive_guarded_and_do_not_apply_raw_forms ... ok
test behavior::scenario_lifecycle::render::tests::birth_label_clip_keeps_padded_world_bounds_at_each_scene_scale ... ok
test behavior::scenario_lifecycle::render::tests::birth_label_clip_preserves_parent_viewport_bounds ... ok
test behavior::scenario_lifecycle::render::tests::box_text_clip_intersects_parent_viewport_and_rejects_empty_interiors ... ok
test behavior::scenario_lifecycle::render::tests::panned_header_and_create_clips_use_screen_translation_and_actual_viewport ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_birth_death_markers_align_endpoints_and_win_selection_without_mutating_source ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_branch_delete_clips_only_its_activation_and_keeps_unanswered_call_visible ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_conditional_repeated_and_parallel_scopes_do_not_claim_definite_continuation ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_empty_and_create_only_use_visible_root_extents_not_insertion_bounds ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_lifecycle_edit_undo_redo_restores_exact_source_and_changed_geometry ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_long_header_title_and_detail_keep_names_and_have_bounded_clips ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_minimum_zoom_marker_halos_do_not_steal_adjacent_capsule_or_header_hits ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_minimum_zoom_near_ordinary_message_wins_over_lifecycle_halos ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_panned_zoomed_program_keeps_header_birth_and_death_hit_identity ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_self_delete_caption_clears_the_preceding_action_box ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_self_lifecycle_elbows_end_on_recorded_marker_rows ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_unassigned_lanes_and_explicit_incomplete_receiver_keep_real_marker_identity ... ok
test behavior::scenario_lifecycle::render::tests::real_scene_wide_latin_header_exceeds_old_character_budget ... ok
test behavior::scenario_lifecycle::tests::alternative_death_does_not_kill_its_displayed_sibling ... ok
test behavior::scenario_lifecycle::tests::conditional_birth_does_not_infer_absence_in_siblings_or_ancestors ... ok
test behavior::scenario_lifecycle::tests::explicit_receiver_is_authoritative_even_with_missing_exchange ... ok
test behavior::scenario_lifecycle::tests::lifecycle_free_fragments_do_not_poison_other_lanes ... ok
test behavior::scenario_lifecycle::tests::loop_lifetime_is_local_and_warning_bearing_loops_remain_drawable ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_saved_action_is_inert_on_replacement_child_review_and_changed_context ... ok
test behavior::scenario_lifecycle::tests::missing_geometry_is_reported_without_nonfinite_or_guessed_markers ... ok
test behavior::scenario_lifecycle::tests::projection_retains_exact_authored_source_and_does_not_enter_history ... ok
test behavior::scenario_lifecycle::tests::optional_cleanup_inside_loop_does_not_claim_balanced_or_definite_exit ... ok
test behavior::scenario_lifecycle::tests::root_lifetime_has_exact_birth_death_boundaries_without_source_mutation ... ok
test behavior::scenario_lifecycle::tests::scenario_use_is_opaque_and_never_imports_callee_lifecycle_events ... ok
test behavior::scenario_scroll::tests::direct_scenario_caller_restores_mounted_scroll_once_with_clamping_and_app_receipt ... ok
test behavior::scenario_scroll::tests::direct_scroll_mounted_identity_resets_navigation_but_preserves_edits_before_first_report ... ok
test behavior::scenario_scroll::tests::direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view ... ok
test behavior::scenario_scroll::tests::independent_process_owner_change_resets_actual_scroll_offsets ... ok
test behavior::scenario_scroll::tests::direct_scroll_observer_reports_actual_initial_clamped_and_remounted_offsets_once ... ok
test behavior::source_draft::layout_tests::contextual_source_layout_long_parent_wraps_after_visible_name_without_losing_raw_data ... ok
test behavior::source_draft::layout_tests::contextual_source_layout_visible_kind_click_keeps_choice_explicit_and_name_input_works ... ok
test behavior::state_canvas::tests::context_menu_keeps_window_anchor_and_explicit_placement_identity ... ok
test behavior::state_canvas::tests::context_tool_and_pending_form_changes_cancel_mutating_gestures ... ok
test behavior::source_draft::layout_tests::contextual_source_layout_initial_essentials_fit_short_composed_host_with_real_paragraphs ... ok
test behavior::state_canvas::tests::creation_uses_the_explicit_deepest_owner_and_never_a_fallback_ancestor ... ok
test behavior::state_canvas::tests::diagram_usage_external_labels_obey_routing_cutoff_while_internal_rows_retain_full_summary ... ok
test behavior::state_canvas::tests::diagram_usage_state_rows_and_label_references_follow_actual_projection ... ok
test behavior::state_canvas::tests::diagram_usage_transition_labels_do_not_match_prose_or_fields_beyond_cutoff ... ok
test behavior::state_canvas::tests::escape_and_view_switch_discard_the_armed_transition_source ... ok
test behavior::state_canvas::tests::internal_rows_and_pseudostate_hits_use_shared_persisted_geometry ... ok
test behavior::state_canvas::tests::drag_uses_ordered_release_at_each_zoom_and_moves_descendants_in_one_undo ... ok
test behavior::state_canvas::tests::ordered_stationary_click_does_not_move_on_later_motion_in_the_same_batch ... ok
test behavior::state_canvas::tests::pan_is_button_scoped_read_only_and_hover_does_not_reproject_the_scene ... ok
test behavior::state_canvas::tests::relayout_does_not_turn_a_transition_click_into_a_drag_or_pan ... ok
test behavior::state_canvas::tests::pseudostate_names_avoid_rightward_and_downward_routes_and_share_hit_bounds ... ok
test behavior::state_canvas::tests::selected_endpoint_reconnect_preserves_identity_and_requires_handle_motion ... ok
test behavior::state_canvas::tests::stacked_symbol_names_avoid_each_other_and_are_independent_of_input_order ... ok
test behavior::state_canvas::tests::selection_relayout_does_not_move_nodes_or_change_drag_distance ... ok
test behavior::state_canvas::tests::transition_supports_click_click_and_drag_without_coalesced_accidental_edges ... ok
test behavior::state_canvas::tests::typed_state_actions_are_named_selectable_rows_before_internal_transitions_and_regions ... ok
test behavior::state_edit::tests::an_empty_machine_has_an_explicit_visible_owner_for_first_region_creation ... ok
test behavior::state_canvas::tests::viewport_uses_full_canvas_coordinates_and_republishes_for_a_new_context ... ok
test behavior::state_edit::tests::connect_preserves_self_loops_and_rejects_hidden_or_incompatible_vertices ... ok
test behavior::state_edit::tests::creation_places_missing_ancestry_and_unfolds_existing_owners_atomically ... ok
test behavior::state_edit::tests::placing_existing_content_and_moving_a_folded_group_preserve_other_views_and_semantics ... ok
test behavior::state_edit::tests::every_context_component_and_invalid_ownership_are_checked_before_planning ... ok
test behavior::state_edit::tests::view_initializes_only_its_machine_in_one_source_and_model_history_entry ... ok
test behavior::state_edit::tests::reconnect_preserves_the_complete_record_and_replaces_cardinality_in_place ... ok
test behavior::states::tests::internal_choice_targets_self_and_edit_preserves_identity_and_description ... ok
test behavior::states::tests::invalid_pseudo_trigger_keeps_draft_and_does_not_create_orphan_event ... ok
test behavior::states::tests::timeout_and_work_trigger_commit_together_roundtrip_and_undo ... ok
test behavior::states::tests::selected_composite_owns_new_region_and_history_uses_selected_region ... ok
test behavior::tests::failed_document_commit_retains_typed_transition_and_time_event_for_repair ... ok
test behavior::tests::legacy_process_and_chain_keep_their_ordered_exchange_scene ... ok
test behavior::tests::recovery_preserves_distinct_qualified_value_paths_and_rejects_invalid_payload_atomically ... ok
test behavior::tests::recovery_retains_each_incomplete_behavior_form_without_executing_it ... ok
test behavior::tests::selecting_state_transition_keeps_its_behavior_context ... ok
test behavior::topology::contextual::tests::qualified_link_gui_deleted_transient_selection_does_not_seed_or_hide_retained_authored_endpoint ... ok
test behavior::topology::contextual::tests::qualified_link_gui_nested_preview_folds_only_the_exact_reference_instance ... ok
test behavior::topology::contextual::tests::qualified_link_gui_rejected_endpoint_keeps_exact_raw_intent_after_recovery ... ok
test behavior::topology::contextual::tests::qualified_link_gui_repeated_sources_remain_distinct_and_apply_has_exact_source_history ... ok
test behavior::tests::one_behavior_form_survives_start_edit_and_unrelated_cancel_until_commit ... ok
test behavior::topology::contextual_routing::tests::contextual_route_native_x_to_first_b_keeps_visible_arrow_above_background_and_clear_of_other_references ... ok
test behavior::topology::contextual_routing::tests::contextual_route_nested_visible_hidden_and_parallel_paths_keep_exact_addresses_and_legacy_routes ... ok
test behavior::topology::contextual_routing::tests::contextual_route_shared_anchor_preserves_selected_direct_reconnect_at_fit_and_full_scale ... ok
test behavior::topology::direct_tests::direct_process_append_preserves_existing_order_and_rejects_incompatible_exchange ... ok
test behavior::topology::direct_tests::direct_process_controls_allow_incomplete_work_and_preserve_branch_semantics ... ok
test behavior::topology::direct_tests::direct_process_empty_first_and_second_exchange_keep_the_same_interactive_renderer ... ok
test behavior::topology::direct_tests::direct_process_imported_order_is_projected_without_mutation_and_first_canvas_edit_is_atomic ... ok
test behavior::topology::direct_tests::direct_process_incompatible_imported_order_rejects_conversion_without_losing_draft ... ok
test behavior::topology::direct_tests::direct_process_nested_source_reopen_and_api_command_use_one_transaction ... ok
test behavior::topology::direct_tests::direct_process_oa_sa_repeated_occurrences_share_sources_and_split_one_guarded_link ... ok
test behavior::topology::direct_tests::direct_process_palette_preserves_fit_and_manual_camera ... ok
test behavior::topology::direct_tests::direct_process_production_click_click_drag_insert_and_camera_coordinates ... ok
test behavior::topology::direct_tests::direct_process_production_endpoint_and_peer_reorder_handles_publish_exact_identity ... ok
test behavior::topology::direct_tests::direct_process_production_stale_focus_scroll_bounds_and_pending_cancel ... ok
test behavior::topology::direct_tests::direct_process_readable_default_fit_expansion_and_bounded_choices ... ok
test behavior::topology::direct_tests::direct_process_real_picker_stays_bounded_and_search_keeps_selection ... ok
test behavior::topology::contextual::tests::qualified_link_gui_scope_paging_and_search_reach_tail_beyond_512_without_losing_endpoints ... ok
test behavior::topology::direct_tests::direct_process_reuse_preview_is_local_expandable_and_recursive_sources_are_rejected ... ok
test behavior::topology::direct_tests::direct_process_reconnect_delete_and_reorder_preserve_other_paths_and_sources ... ok
test behavior::topology::direct_tests::direct_process_specific_parallel_link_and_incompatible_adjacency ... ok
test behavior::topology::direct_tests::direct_process_scaled_control_and_link_gestures_use_visible_positions ... ok
test behavior::topology::tests::cycle_projection_terminates_and_retains_actual_links ... ok
test behavior::topology::tests::empty_process_can_start_through_normal_authoring_events ... ok
test behavior::topology::direct_tests::direct_process_three_ordered_exchanges_preserve_outer_paths_on_insertion ... ok
test behavior::topology::tests::fitting_a_branch_keeps_all_nodes_visible_and_selection_uses_scaled_coordinates ... ok
test behavior::topology::tests::guarded_branch_is_one_transaction_with_distinct_source_identities ... ok
test behavior::topology::tests::insertion_picker_only_offers_work_matching_adjacent_exchanges ... ok
test behavior::topology::tests::legacy_branch_insertion_replaces_only_the_selected_implied_link ... ok
test behavior::topology::tests::long_engineering_identifiers_stay_inside_label_width ... ok
test behavior::topology::tests::process_camera_selection_ignores_independent_details_scroll ... ok
test behavior::topology::tests::legacy_edit_preserves_order_and_new_work_in_one_undo ... ok
test behavior::topology::tests::process_fit_uses_measured_height_and_reports_minimum_without_changing_manual_zoom ... ok
test behavior::topology::tests::reusable_preview_preserves_identity_and_filters_recursive_sources ... ok
test behavior::topology::tests::process_fit_disclosures_and_focus_preserve_rejected_raw_draft_and_accepted_source ... ok
test behavior::topology::tests::unlinked_contributions_do_not_gain_fictional_edges ... ok
test behavior::topology::tests::stale_or_invalid_submit_preserves_inputs_and_creates_nothing ... ok
test behavior::topology::usages::tests::expanded_nested_tiles_do_not_invent_links_the_preview_painter_does_not_emit ... ok
test behavior::topology::usages::tests::preview_arrow_focus_is_in_the_painted_gap_and_hidden_endpoints_never_fallback ... ok
test behavior::topology::usages::tests::process_contextual_purpose_and_painted_preview_links_keep_distinct_identities ... ok
test behavior::topology::usages::tests::process_usage_reveal_validates_before_state_changes_and_centers_the_exact_tile ... ok
test behavior::topology::usages::tests::process_usages_keep_caller_paths_and_exclude_unshown_fifth_preview ... ok
test behavior::topology::usages::tests::process_usages_exclude_endpoint_coincidence_and_keep_actual_occurrences_and_links ... ok
test behavior::topology::usages::tests::visible_preview_segment_excludes_all_opaque_tiles_for_reverse_and_cross_row_arrows ... ok
test behavior::topology::view::layout_tests::production_process_caller_return_rejects_its_previous_camera_lifetime ... ok
test behavior::topology::view::layout_tests::production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_review_owners ... ok
test behavior::topology::view::layout_tests::production_process_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok
test behavior::topology::view::layout_tests::production_process_canvas_focus_wheel_fit_and_stale_camera_are_presentation_only ... ok
test behavior::topology::view::layout_tests::production_process_form_replacement_resets_scroll_without_a_closed_render ... ok
test behavior::topology::view::layout_tests::production_process_canvas_scaled_path_creation_preserves_source_and_editor_bounds ... ok
test behavior::topology::view::layout_tests::production_process_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok
test behavior::topology::view::layout_tests::production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok
test connection_edit::tests::cancel_navigation_revision_and_dirty_drafts_cannot_commit_a_stale_connection ... ok
test connection_edit::tests::visible_endpoint_drawing_commits_ports_and_exchange_once_with_exact_history ... ok
test connection_tool::tests::boundary_candidates_match_core_orientation_and_reject_same_owner_or_mixed_kind ... ok
test connection_tool::tests::boundary_feasibility_keeps_interface_warnings_and_final_carried_item_checks ... ok
test connection_tool::tests::boundary_feasibility_rejects_dangling_identity_in_a_malformed_element_map ... ok
test connection_tool::tests::direct_connections_share_exact_source_history_and_model_parity ... ok
test connection_tool::tests::explicit_function_ports_keep_owners_contracts_and_do_not_invent_carried_items ... ok
test connection_tool::tests::final_command_validation_rejects_invalid_models_without_mutation ... ok
test connection_tool::tests::function_cards_use_unique_reuse_and_atomic_generation_when_ambiguous ... ok
test connection_tool::tests::mismatched_port_owners_layers_and_contract_kinds_are_not_candidates ... ok
test connection_tool::tests::operational_communication_uses_participants_without_creating_ports ... ok
test connection_tool::tests::operational_interaction_targets_only_distinct_oa_activities ... ok
test context_menu::tests::a_new_context_token_cannot_reuse_the_previous_mouse_press_or_scroll ... ok
test context_menu::tests::actions_area_does_not_swallow_first_independent_click_after_overlay_consumed_its_release ... ok
test context_menu::tests::anchors_outside_the_window_and_nonfinite_input_stay_bounded ... ok
test context_menu::tests::clicking_disabled_or_dragging_off_an_action_never_executes_it ... ok
test context_menu::tests::coalesced_dismissal_release_does_not_swallow_the_next_independent_click ... ok
test context_menu::tests::empty_and_disabled_only_menus_still_capture_keys_and_escape_closes ... ok
test behavior::topology::view::layout_tests::production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok
test context_menu::tests::keyboard_skips_headers_separators_and_disabled_actions_and_invokes_once ... ok
test context_menu::tests::reconstructed_overlay_uses_retained_layout_for_rows_and_hit_testing ... ok
test context_menu::tests::outside_dismissal_consumes_press_and_release_across_controlled_close ... ok
test context_menu::tests::tall_menu_clamps_to_viewport_and_keyboard_wheel_and_scrollbar_reach_the_end ... ok
test data_values::tests::phase_choices_keep_reused_record_addresses_distinct ... ok
test data_values::tests::recursive_picker_starts_with_distinct_leaf_names_and_sorts_them ... ok
test context_menu::tests::retained_overlay_layout_keeps_measured_width_and_scrollbar_after_reconstruction ... ok
test data_values::tests::reference_parameter_stops_descent_and_unspecified_direction_remains_visible ... ok
test data_values::tests::recursive_records_can_be_drilled_into_and_valid_imported_paths_resolve ... ok
test data_view::tests::allocation_rows_edit_relationship_identity_and_remove_only_that_allocation ... ok
test data_view::tests::association_creation_is_a_separate_relation_with_editable_end_contracts ... ok
test data_view::tests::browsing_pages_preserves_invalid_numeric_draft_and_inherited_allocation_identity ... ok
test data_view::tests::contextual_data_actions_use_typed_defaults_and_one_undo ... ok
test data_values::tests::roots_and_one_hop_children_do_not_expand_unrelated_payload_branches ... ok
test diagram::lifecycle_tests::menu_reset_and_draw_use_fresh_open_context ... ok
test diagram::lifecycle_tests::middle_and_arrow_panning_preserve_source_and_history ... ok
test diagram::lifecycle_tests::motion_after_release_must_not_move_clicked_port ... ok
test diagram::lifecycle_tests::motion_after_source_click_release_must_not_create_connection ... ok
test diagram::pointer_tests::coalesced_fe_ports_commit_once_through_app_and_source_history ... ok
test diagram::pointer_tests::coalesced_port_drag_commits_once_through_app_and_source_history ... ok
test diagram::pointer_tests::drag_from_already_selected_stationary_port_keeps_source_between_batches ... ok
test diagram::pointer_tests::menu_captured_motion_must_not_turn_following_click_into_old_port_drag ... ok
test diagram::pointer_tests::modal_and_stale_epoch_block_queued_connection_and_port_commits ... ok
test diagram::port_hit_tests::coincident_ports_keep_selection_then_use_stable_identity ... ok
test diagram::port_hit_tests::nearest_port_center_wins_on_all_borders_and_zoom_levels ... ok
test diagram::port_hit_tests::nested_owner_palette_selection_drag_and_context_menu_pick_the_same_port ... ok
test diagram::port_labels::tests::leaving_and_reentering_keeps_suppression_until_each_held_button_releases ... ok
test diagram::port_labels::tests::menu_epoch_clears_suppression_when_the_overlay_consumes_release ... ok
test diagram::port_labels::tests::outside_presses_do_not_suppress_and_cancel_or_focus_loss_clear_held_buttons ... ok
test diagram::port_labels::tests::passive_layer_leaves_port_drag_selection_and_commit_with_the_interactive_canvas ... ok
test diagram::tests::association_end_contracts_share_svg_geometry_and_select_without_moving_name_label ... ok
test diagram::tests::bend_gestures_use_screen_hit_radius_and_commit_one_undo_at_three_zooms ... ok
test diagram::tests::coincident_line_hit_retains_the_selected_connection ... ok
test diagram::tests::connection_gestures_pick_real_ports_and_commit_coalesced_drag_at_every_zoom ... ok
test diagram::tests::detached_label_selects_connection_and_occluding_card_owns_its_hit ... ok
test diagram::tests::dragging_a_child_previews_the_same_enclosure_and_ports_as_commit ... ok
test diagram::tests::fast_drags_commit_the_release_cursor_without_a_final_motion_event ... ok
test diagram::tests::implicit_port_click_selects_semantics_and_drag_preview_svg_share_anchors ... ok
test diagram::tests::route_cache_ignores_pan_zoom_and_cancelled_drag_cannot_commit ... ok
test diagram::tests::routed_svg_has_same_polyline_label_and_bounds_as_canvas_scene ... ok
test diagram::tests::svg_distinguishes_inferred_work_allocation_from_direct_allocation ... ok
test diagram::tests::svg_preserves_visible_exchange_labels_and_capability_stereotypes ... ok
test diagram::tests::typed_function_glyphs_and_unoriented_standard_connections_have_distinct_svg ... ok
test diagram::view_options_tests::communication_svg_uses_saved_visibility_labels_and_direction ... ok
test diagram_usage::coverage_tests::absent_endpoint_placements_and_hidden_performer_do_not_create_grouping_targets ... ok
test diagram_usage::coverage_tests::allocation_grouping_has_exact_relation_placements_and_readable_work_geometry ... ok
test diagram_usage::coverage_tests::allocation_grouping_walks_two_visible_summary_levels_without_inventing_allocations ... ok
test diagram_usage::coverage_tests::collapsed_leaf_and_edge_filter_still_leave_the_actual_nesting_visible ... ok
test diagram_usage::coverage_tests::folded_summary_or_performer_does_not_substitute_a_visible_ancestor_for_the_leaf ... ok
test diagram_usage::coverage_tests::other_projections_do_not_report_allocation_nesting_when_only_an_arrow_or_nothing_is_drawn ... ok
test diagram_usage::coverage_tests::repeated_saved_views_keep_allocation_placement_identity_and_geometry_independent ... ok
test diagram_usage::coverage_tests::role_derived_nesting_without_a_direct_allocation_does_not_invent_one ... ok
test diagram_usage::tests::folded_child_is_not_a_visible_node_and_placement_identity_stays_exact ... ok
test diagram_usage::tests::oversized_container_reveal_targets_its_labelled_header ... ok
test diagram_usage::tests::ports_are_found_at_the_visible_owner_and_unplaced_elements_do_not_match_names ... ok
test diagram_usage::tests::reveal_camera_centers_target_without_whole_view_shrink ... ok
test diagram_usage::tests::saved_membership_respects_filters_and_does_not_change_project ... ok
test diagram_usage::tests::state_machine_owner_finds_its_view_without_a_machine_placement ... ok
test diagram_usage::tests::whole_diagram_camera_fits_large_bounds_instead_of_individual_object_scale ... ok
test document::save_as_tests::save_as_preserves_explicit_source_entry_changes_in_history ... ok
test document::save_as_tests::save_as_rebases_uniform_history_without_an_undo_step_or_lost_redo ... ok
test document::save_as_tests::save_as_rejects_included_filename_collisions_in_past_and_future_without_mutation ... ok
test document::save_as_tests::save_as_rejects_stale_or_content_changing_receipt_atomically ... ok
test document::tests::accepted_history_keeps_the_latest_hundred_complete_snapshots ... ok
test document::tests::altered_compiled_model_or_index_is_rejected_without_mutation ... ok
test document::tests::asynchronous_save_marks_its_snapshot_without_cleaning_later_edits ... ok
test document::tests::byte_budget_trims_oldest_history_without_discarding_a_large_current ... ok
test document::tests::cached_view_index_tracks_accepted_creation_undo_and_redo ... ok
test document::tests::comment_and_semantic_history_restore_exact_bytes_and_branch_together ... ok
test document::tests::creation_selects_the_single_uuid_persisted_in_source ... ok
test document::tests::invalid_project_is_rejected_before_format_can_normalize_it ... ok
test document::tests::nested_cross_file_reparent_and_source_edit_share_exact_history ... ok
test document::tests::no_op_command_or_source_apply_preserves_history_revision_and_redo ... ok
test document::tests::omitted_id_stabilization_remains_dirty_until_exact_source_is_saved ... ok
test document::tests::recreated_equal_record_does_not_inherit_deleted_source_ownership ... ok
test document::tests::rejected_compile_command_and_writeback_preserve_state_and_redo ... ok
test explorer::tests::collapse_and_search_retain_context_without_changing_collapse_state ... ok
test explorer::tests::deep_containment_and_search_keep_the_full_path_without_recursion ... ok
test explorer::tests::equal_names_have_stable_alias_and_identity_tie_breakers ... ok
test explorer::tests::perspective_isolation_and_allocations_do_not_change_containment ... ok
test explorer::tests::prefix_names_cannot_separate_a_parent_from_its_children ... ok
test forms::data_fields::tests::defaults_distinguish_absent_empty_and_typed_literals_with_escaped_unicode ... ok
test forms::data_fields::tests::field_browser_pages_every_direct_field_without_flattening_other_records ... ok
test forms::data_fields::tests::independent_review_combined_take_type_informs_actual_default_form_in_both_directions ... ok
test forms::data_fields::tests::numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state ... ok
test forms::data_fields::tests::selecting_qualified_path_preserves_expression_typed_before_the_next_render ... ok
test forms::data_fields::tests::semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep ... ok
test forms::data_fields::tests::supertype_picker_excludes_descendants_and_keeps_original_inherited_field_identity ... ok
test forms::history_fields::tests::history_absent_baseline_and_empty_history_use_explicit_facts_and_exact_document_history ... ok
test forms::history_fields::tests::history_clear_discards_inactive_invalid_constructors_before_reusing_the_same_row ... ok
test forms::history_fields::tests::history_compound_value_and_constructor_variants_are_editable_without_live_context ... ok
test forms::history_fields::tests::history_deleted_keys_and_observation_uuids_remain_exact_and_duplicate_history_rejects ... ok
test forms::history_fields::tests::history_every_descriptor_fact_choice_matches_actual_closed_types_and_source_contract ... ok
test forms::history_fields::tests::history_exact_strings_do_not_confuse_uuid_shaped_prose_or_literal_keys_with_references ... ok
test forms::history_fields::tests::history_invalid_numbers_survive_constructor_switch_reorder_removal_and_recovery ... ok
test forms::history_fields::tests::history_missing_semantic_field_can_be_added_and_removed_without_touching_prose_or_live_objects ... ok
test forms::history_fields::tests::history_nested_optional_number_events_switch_absent_present_and_recover_invalid_raw ... ok
test forms::history_fields::tests::history_nested_uuid_and_list_events_preserve_present_values_and_tagged_item_branches ... ok
test forms::history_fields::tests::history_optional_uuid_and_text_seed_absence_without_inventing_observations ... ok
test forms::history_fields::tests::history_primitive_type_choices_remain_visible_inside_the_tagged_union ... ok
test forms::history_fields::tests::history_retained_text_constructors_follow_exact_mode_without_changing_the_observation ... ok
test forms::port_fields::tests::all_demo_records_roundtrip_through_graphical_and_escaped_string_drafts ... ok
test forms::port_fields::tests::boundary_choices_honor_draft_direction_same_kind_and_direct_interface_items ... ok
test forms::port_fields::tests::contract_choices_follow_owner_and_never_offer_legacy_or_oa_typed_ports ... ok
test forms::port_fields::tests::failed_shape_keeps_incomplete_text_and_saved_contract_payloads ... ok
test forms::port_fields::tests::functional_endpoints_require_owner_direction_and_items_but_retain_bad_current_choice ... ok
test forms::port_fields::tests::interface_roles_offer_participants_and_component_ports_independent_of_orientation ... ok
test forms::port_fields::tests::port_allocation_choices_use_actual_owner_direction_and_ignore_edited_relation ... ok
test forms::port_fields::tests::standard_switch_retains_incompatible_orientation_until_explicit_repair ... ok
test forms::port_fields::tests::switching_function_direction_preserves_invalid_items_and_each_authored_payload ... ok
test forms::reference_chooser::tests::captured_choice_cannot_cross_document_revision_draft_record_or_array_path ... ok
test forms::reference_chooser::tests::catalog_search_disambiguates_repeated_names_containment_alias_and_exact_placement_identity ... ok
test forms::reference_chooser::tests::dismissal_swallows_matching_release_across_closed_rebuild_but_next_click_reaches_parent ... ok
test forms::reference_chooser::tests::empty_search_escape_tab_and_optional_clear_do_not_silently_choose_nil ... ok
test forms::reference_chooser::tests::historical_paths_never_open_and_only_explicit_choice_or_clear_is_a_write ... ok
test forms::reference_chooser::tests::mounted_clipboard_paste_and_ime_commit_preserve_whole_strings_without_selecting ... ok
test forms::reference_chooser::tests::mounted_filtered_row_click_uses_the_displayed_identity_and_scoped_write_intent ... ok
test forms::reference_chooser::tests::mounted_input_preserves_rapid_same_batch_typing_and_cursor_across_controlled_rebuilds ... ok
test forms::reference_chooser::tests::mounted_keyboard_pages_and_selects_only_the_highlighted_visible_candidate_once ... ok
test forms::reference_chooser::tests::mounted_overlay_clamps_at_three_field_positions_and_keeps_results_and_query_bounded ... ok
test forms::reference_chooser::tests::mounted_query_change_rejects_old_release_and_old_result_events_without_locking_popup ... ok
test forms::reference_chooser::tests::mounted_query_then_escape_in_one_batch_closes_and_relative_keys_keep_input_order ... ok
test forms::reference_chooser::tests::out_of_order_query_serials_cannot_restore_obsolete_results ... ok
test forms::tests::data_and_port_choices_are_typed_but_retain_invalid_current_values ... ok
test forms::tests::editing_allocation_offers_other_performers_without_counting_itself_twice ... ok
test forms::tests::moving_placement_and_changing_identity_is_one_lossless_undoable_edit ... ok
test forms::tests::parent_choices_reject_cycles_without_removing_clear_or_current_choice ... ok
test forms::tests::reply_and_reuse_forms_offer_semantically_compatible_references ... ok
test forms::tests::scenario_choices_respect_owner_and_exchange_layer ... ok
test forms::tests::semantic_port_group_choices_use_combined_owner_and_retain_invalid_transferred_items ... ok
test forms::tests::shared_perspective_change_keeps_payload_and_rejects_incompatible_dependencies ... ok
test forms::tests::state_observation_picker_keeps_work_lifelines_allocated_to_machine_owner ... ok
test forms::tests::state_placement_form_uses_draft_destination_membership_without_changing_other_views ... ok
test forms::tests::state_projection_form_keeps_string_variants_and_requires_a_typed_context ... ok
test forms::view_fields::tests::category_family_change_retains_members_until_explicit_correction ... ok
test forms::view_fields::tests::selective_view_property_apply_recovery_and_undo_preserve_semantics_and_empty_choices ... ok
test guide::tests::filtering_and_related_navigation_keep_selection_visible ... ok
test guide::tests::guide_topic_navigation_resets_only_article_and_same_topic_search_keeps_scroll ... ok
test guide::tests::search_finds_concepts_in_walkthroughs_and_api_reference ... ok
test home::tests::corrupt_future_and_oversized_history_are_preserved_with_usable_session_history ... ok
test home::tests::failed_metadata_replace_keeps_previous_history_and_reports_memory_only_entry ... ok
test home::tests::first_and_replaced_metadata_are_parseable_and_contain_no_model_bytes ... ok
test home::tests::forgotten_and_missing_files_never_change_the_model_or_other_entries ... ok
test home::tests::loading_missing_history_does_not_create_files_or_directories ... ok
test home::tests::malformed_entries_and_unknown_fields_never_become_open_requests ... ok
test home::tests::successful_local_history_is_bounded_ordered_deduplicated_and_durable ... ok
test home::tests::unavailable_location_can_reappear_on_refresh_without_losing_identity ... ok
test home::tests::walkthrough_uses_packaged_bundle_before_development_and_reports_missing_assets ... ok
test hosted::client::tests::hosted_endpoint_validation_redacts_secrets_and_preserves_proxy_paths ... ok
test hosted::client::tests::hosted_lost_publication_response_is_uncertain_without_retry ... ok
test hosted::client::tests::hosted_open_rejects_incoherent_envelopes_missing_etags_and_unstable_ids ... ok
test hosted::client::tests::hosted_oversized_local_bundle_is_rejected_before_network_admission ... ok
test hosted::client::tests::hosted_production_auth_proxy_prefix_and_located_rejection ... ok
test hosted::client::tests::hosted_production_roundtrip_preserves_comments_noops_and_conflicts ... ok
test data_view::tests::large_inherited_dictionary_has_bounded_widgets_and_every_original_field_is_reachable has been running for over 60 seconds
test hosted::client::tests::hosted_publication_race_keeps_receipt_identity_for_review ... ok
test hosted::client::tests::hosted_publication_retains_known_resource_after_failed_followup ... ok
test hosted::client::tests::hosted_put_preserves_an_opaque_etag_and_compiles_the_accepted_bundle ... ok
test hosted::client::tests::hosted_redirect_is_not_followed_or_retried_and_problem_tokens_are_redacted ... ok
test hosted::client::tests::hosted_response_limits_cover_declared_and_chunked_bodies ... ok
test hosted::client::tests::hosted_structured_mutation_failures_distinguish_precommit_rejections ... ok
test hosted::client::tests::hosted_timeout_and_disconnect_leave_mutation_outcomes_explicit ... ok
test model_menu::tests::add_bend_here_uses_context_point_and_one_source_aware_history_change ... ok
test model_menu::tests::late_menu_actions_do_not_dismiss_new_menu_or_cross_view_context ... ok
test model_menu::tests::menu_preserves_pending_properties_and_rejects_delayed_mutations ... ok
test model_menu::tests::pending_source_allows_file_navigation_but_disables_model_actions ... ok
test model_menu::tests::right_click_targets_actor_without_changing_selection_until_action ... ok
test model_menu::tests::show_from_explorer_places_unfolds_and_keeps_semantic_selection ... ok
test model_menu::tests::state_menu_folds_and_hides_only_its_captured_placement_and_rejects_stale_views ... ok
test model_menu::tests::state_menu_keeps_typed_creation_reachable_and_preserves_pending_properties ... ok
test navigation::tests::semantic_browser_finds_behavior_users_without_confusing_containment ... ok
test port_edit::tests::all_border_drags_commit_exact_preview_and_restore_automatic_anchors_in_history ... ok
test port_edit::tests::graphical_port_reparent_clears_old_anchors_in_the_same_history_operation ... ok
test port_preview::tests::functional_creation_identifies_new_and_reused_endpoints_and_owners ... ok
test port_preview::tests::legacy_split_discloses_endpoint_routing_item_origins_and_retained_evidence ... ok
test port_preview::tests::reused_boundary_shows_item_union_and_existing_allocating_facts ... ok
test port_preview::tests::synchronization_preview_exposes_contract_shrinkage_and_current_declaration_origin ... ok
test port_workflow::recovery_tests::port_recovery_retains_authored_contract_and_identity_but_rebuilds_preview ... ok
test port_workflow::tests::creating_typed_port_and_rejecting_stale_preview_preserve_drafts_and_identity ... ok
test port_workflow::tests::graphical_function_exchange_and_component_generation_commit_and_undo_whole_contracts ... ok
test port_workflow::tests::legacy_resolution_preserves_original_record_and_applies_as_one_undo ... ok
test port_workflow::tests::port_creation_keeps_owner_diagram_and_unfolds_in_the_same_undo ... ok
test port_workflow::tests::synchronization_uses_active_contract_instead_of_preserved_direction ... ok
test recovery::source5::tests::source5_fingerprint_preserves_historical_field_order_and_diagram_shape ... ok
test recovery::source5_tests::source4_checkpoint_remains_explicitly_unsupported_without_touching_disk ... ok
test recovery::source5_tests::source5_checkpoint_does_not_refresh_unrelated_clean_buffer_baseline ... ok
test recovery::source5_tests::source5_checkpoint_missing_identity_is_not_a_lossless_upgrade ... ok
test recovery::source5_tests::source5_checkpoint_old_diagram_property_shape_remains_editable ... ok
test recovery::source5_tests::source5_checkpoint_port_draft_replans_and_rejects_stale_base ... ok
test recovery::source5_tests::source5_checkpoint_preserves_dirty_invalid_text_and_baseline_exactly ... ok
test recovery::source5_tests::source5_checkpoint_preserves_distinct_unapplied_open_workspace ... ok
test recovery::source5_tests::source5_checkpoint_property_preserves_invalid_raw_input ... ok
test recovery::source5_tests::source5_checkpoint_upgrade_never_rewrites_checkpoint_or_origin ... ok
test recovery::source5_tests::source5_checkpoint_upgrades_only_header_and_clean_editor_baseline ... ok
test recovery::source5_tests::source5_checkpoint_workflow_rebinds_only_the_proven_old_revision ... ok
test recovery::source7_tests::source7_current_combined_contextual_and_operation_drafts_survive_without_execution ... ok
test recovery::source7_tests::source7_explicit_new_fields_cannot_enter_frozen_old_model_profiles ... ok
test recovery::source7_tests::source7_legacy_inactive_constructor_path_is_structural_but_raw_prose_is_not ... ok
test data_view::tests::large_inherited_dictionary_has_bounded_widgets_and_every_original_field_is_reachable ... ok
test recovery::source7_tests::source7_legacy_profiles_preserve_dirty_source_property_and_behavior_raw_fields ... ok
test recovery::source7_tests::source7_legacy_structural_intent_rejects_even_null_but_never_scans_raw_strings ... ok
test recovery::source7_tests::source7_legacy_view_selector_refuses_new_relation_kind_without_rewriting_checkpoint ... ok
test recovery::source7_tests::source7_legacy_profile_workflow_and_semantic_drafts_replan_with_exact_base ... ok
test recovery::tests::altered_accepted_identity_and_unrecognized_raw_property_fields_reject ... ok
test recovery::tests::exact_source_and_unfinished_invalid_buffers_restore_without_model_overwrite ... ok
test recovery::source7_tests::source7_recovers_nonempty_frozen_5_and_6_profiles_with_exact_historical_binding ... ok
test recovery::tests::excess_retained_buffers_cannot_replace_the_last_recoverable_checkpoint ... ok
test recovery::tests::invalid_open_retains_distinct_accepted_model_and_repair_workspace ... ok
test recovery::tests::missing_includes_remain_reachable_repair_targets_without_fabricated_buffers ... ok
test recovery::tests::raw_invalid_property_number_survives_and_stays_rejected_until_repaired ... ok
test recovery::tests::multiple_sessions_replace_only_their_own_checkpoint_and_corruption_is_retained ... ok
test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_draft_cannot_target_an_unreserved_or_non_artifact_identity ... ok
test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_raw_variants_duplicate_addresses_and_unknown_fields_are_refused ... ok
test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_reservations_cannot_duplicate_or_claim_an_existing_identity_or_another_owner ... ok
test semantic_review::artifacts_tests::artifact_candidate_is_a_complete_record_not_a_tombstone_or_unattached_creation_shortcut ... ok
test semantic_review::artifacts_tests::artifact_creation_and_identified_source_deletion_follow_core_metadata_and_history_boundary_both_directions ... ok
test semantic_review::artifacts_recovery_falsifier_tests::legitimate_invalid_artifact_checkpoint_round_trips_and_prepares_only_after_explicit_repair ... ok
test semantic_review::artifacts_tests::artifact_metadata_apply_and_exact_history_reset_are_one_source_preserving_undo_redo ... ok
test semantic_review::artifacts_tests::artifact_gui_reserves_recovers_and_creates_only_after_all_seven_metadata_reviews_both_directions ... ok
test semantic_review::artifacts_tests::artifact_move_metadata_none_empty_and_invalid_raw_recovery_are_independent_both_directions ... ok
test semantic_review::attachments::tests::absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exact_legacy_base ... ok
test semantic_review::artifacts_tests::artifact_moved_source_absence_deletes_but_destination_only_take_retains_both_directions ... ok
test semantic_review::attachments::tests::attachment_apply_is_one_exact_source_and_history_undo_redo ... ok
test semantic_review::attachments::tests::addressed_attachment_candidates_equal_direct_core_oracle_both_directions ... ok
test semantic_review::attachments::tests::attachment_pair_drafts_survive_family_switch_and_restore_without_mutating_model ... ok
test semantic_review::attachments::tests::functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate ... ok
test semantic_review::attachments::tests::parent_and_each_endpoint_candidate_preserve_other_fields_and_match_core_both_directions ... ok
test semantic_review::attachments::tests::raw_rejected_attachment_reference_survives_recovery_and_wrong_scope_is_rejected ... ok
test semantic_review::attachments::tests::source_only_absence_and_explicit_absent_endpoint_are_different ... ok
test semantic_review::attachments::tests::attachment_candidate_and_direct_record_keep_conflict_on_actual_slot_without_losing_drafts ... ok
test semantic_review::attachments::tests::two_incident_endpoint_candidates_form_one_complete_record_and_recover_invalid_excluded_draft ... ok
test semantic_review::batch_tests::batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip ... ok
test semantic_review::batch_tests::batch_candidate_conflict_keeps_both_raw_values_and_keep_is_a_real_constraint ... ok
test semantic_review::batch_tests::batch_complete_constructor_cannot_erase_another_rows_new_literal ... ok
test semantic_review::batch_tests::batch_constructor_retirement_conflicts_with_another_rows_kept_bound_in_both_orders ... ok
test semantic_review::batch_tests::batch_delayed_text_targets_its_original_row_and_facet_without_switching_focus ... ok
test semantic_review::batch_tests::batch_excluded_invalid_row_survives_focus_reopen_and_v2_recovery ... ok
test semantic_review::batch_tests::batch_first_equal_constructor_acknowledgement_allows_an_independent_bound ... ok
test semantic_review::batch_tests::batch_joint_bounds_use_one_candidate_and_match_an_independent_atomic_core_oracle ... ok
test semantic_review::batch_tests::batch_reverse_distinct_targets_work_and_reverse_convergence_is_explicit ... ok
test semantic_review::batch_tests::batch_v2_rejects_duplicate_included_sources_bad_addresses_and_stale_model ... ok
test semantic_review::batch_tests::recovered_trace_picker_names_existing_trace_instead_of_deleted_reference ... ok
test semantic_review::batch_tests::category_family_and_members_are_independent_core_checked_facets_both_directions ... ok
test semantic_review::compose::diagnostic_tests::distinct_review_conflict_keeps_trace_aliases_even_when_names_are_equal ... ok
test semantic_review::compose::diagnostic_tests::reserved_creation_conflict_uses_its_rows_seed_instead_of_a_deleted_reference ... ok
test semantic_review::compose::diagnostic_tests::same_review_presence_and_owner_conflict_names_both_facets_and_owners_and_retains_drafts ... ok
test semantic_review::compose::diagnostic_tests::typed_owner_display_does_not_reinterpret_uuid_shaped_artifact_text ... ok
test semantic_review::batch_tests::rejected_direction_switch_at_row_limit_preserves_every_recoverable_draft has been running for over 60 seconds
test semantic_review::constructor_tests::closed_data_and_event_take_composition_matches_core_across_every_kind_pair_and_direction has been running for over 60 seconds
test semantic_review::constructor_tests::closed_data_and_event_take_composition_matches_core_across_every_kind_pair_and_direction ... ok
test semantic_review::constructor_tests::data_group_candidates_cover_every_definition_and_companion_default_both_directions ... ok
test semantic_review::constructor_tests::data_group_retains_invalid_sibling_drafts_companion_default_and_variant_editor_buffers ... ok
test semantic_review::constructor_tests::exchange_constructor_takes_qualified_annotation_addresses_without_remapping_expression_text ... ok
test semantic_review::constructor_tests::existing_fragment_and_branch_group_candidates_edit_complete_owned_order_without_deletion ... ok
test semantic_review::constructor_tests::group_take_reference_payload_and_scalar_candidate_match_core_and_one_document_batch ... ok
test semantic_review::constructor_tests::historical_event_keep_retires_only_obsolete_branch_and_unchanged_kind_acknowledges_no_payload ... ok
test semantic_review::batch_tests::rejected_direction_switch_at_row_limit_preserves_every_recoverable_draft ... ok
test semantic_review::constructor_tests::historical_same_kind_take_preserves_target_only_literals_when_composed_with_candidate ... ok
test semantic_review::constructor_tests::outer_and_nested_constructor_candidates_require_explicit_resolution_and_retain_both_drafts ... ok
test semantic_review::constructor_tests::port_group_companions_typed_owner_context_and_direction_restrictions_are_explicit ... ok
test semantic_review::constructor_tests::reverse_existing_typed_port_reviews_keep_or_candidate_legacy_without_permitting_typed_oa_write ... ok
test semantic_review::constructor_tests::scenario_use_group_takes_typed_binding_references_and_preserves_source_text_both_directions ... ok
test semantic_review::keyed_rows_tests::annotation_address_browser_descends_qualified_fields_without_changing_rows ... ok
test semantic_review::keyed_rows_tests::annotation_observations_keep_uuid_shaped_expression_text_in_both_sides_and_history ... ok
test semantic_review::constructor_tests::occurrence_and_event_candidate_variants_cover_closed_families_without_touching_headers ... ok
test semantic_review::keyed_rows_tests::binding_choices_separate_reused_keys_from_local_lifelines ... ok
test semantic_review::keyed_rows_tests::binding_local_picker_follows_a_candidate_parent_before_the_review_is_valid ... ok
test semantic_review::keyed_rows_tests::binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenario ... ok
test semantic_review::keyed_rows_tests::batch_keyed_rows_compare_exact_target_slots_and_retain_independent_values ... ok
test semantic_review::keyed_rows_tests::independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_directions has been running for over 60 seconds
test semantic_review::keyed_rows_tests::keyed_recovery_retains_raw_rows_and_staged_pairs_both_directions_without_applying ... ok
test semantic_review::keyed_rows_tests::keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_staging ... ok
test semantic_review::keyed_rows_tests::independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_directions ... ok
test semantic_review::keyed_rows_tests::keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exact_reset ... ok
test semantic_review::keyed_rows_tests::row_review_apply_preserves_exact_source_and_semantic_history_through_undo_redo ... ok
test semantic_review::keyed_tests::deleted_member_source_observation_removes_only_mapped_membership_both_directions ... ok
test semantic_review::keyed_rows_tests::multiple_new_rows_compose_in_empty_collections_using_destination_key_order ... ok
test semantic_review::keyed_tests::exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions ... ok
test semantic_review::keyed_tests::exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss ... ok
test semantic_review::keyed_tests::function_port_members_use_existing_typed_choices_and_preserve_oa_legacy ... ok
test semantic_review::keyed_tests::historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution ... ok
test semantic_review::keyed_tests::member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys ... ok
test semantic_review::keyed_tests::invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable ... ok
test semantic_review::keyed_tests::staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence ... ok
test semantic_review::keyed_tests::reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft ... ok
test semantic_review::keyed_tests::stale_or_source_failed_reset_and_membership_apply_retain_exact_choices ... ok
test semantic_review::keyed_tests::unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority ... ok
test semantic_review::message_kind_tests::message_group_candidate_closed_kinds_are_core_validated_in_both_directions ... ok
test semantic_review::message_kind_tests::message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit ... ok
test semantic_review::message_kind_tests::message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement ... ok
test semantic_review::keyed_tests::unordered_members_keep_take_candidate_preserve_other_rows_both_directions has been running for over 60 seconds
test semantic_review::message_kind_tests::message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects ... ok
test semantic_review::message_kind_tests::message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer ... ok
test semantic_review::message_kind_tests::message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions ... ok
test semantic_review::message_kind_tests::message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions ... ok
test semantic_review::message_kind_tests::message_group_timer_does_not_erase_exchange_without_its_own_selected_decision ... ok
test semantic_review::keyed_tests::unordered_members_keep_take_candidate_preserve_other_rows_both_directions ... ok
test semantic_review::ordered_tests::historical_pair_restore_uses_exact_member_keys_and_never_zips_order_observations ... ok
test semantic_review::ordered_tests::order_take_preserves_destination_only_positions_and_order_alone_cannot_add_members ... ok
test semantic_review::ordered_tests::ambiguous_order_preserves_local_member_and_requires_explicit_candidate_positions ... ok
test semantic_review::ordered_tests::ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes ... ok
test semantic_review::ordered_tests::ordered_review_apply_is_exact_source_history_transaction_with_reopen_and_undo_redo ... ok
test semantic_review::ordered_tests::ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_take_reorders_only ... ok
test semantic_review::ordered_tests::raw_order_survives_membership_changes_decisions_and_recovery_until_explicit_reconcile ... ok
test semantic_review::ordered_tests::ordered_take_uses_unique_anchors_and_matches_core_with_unrelated_candidate_both_directions ... ok
test semantic_review::recovery::tests::raw_scalar_and_closed_constructor_drafts_recover_against_exact_model_only ... ok
test semantic_review::recovery::tests::version_one_without_orders_restores_raw_intent_and_replans_without_execution ... ok
test semantic_review::source7_tests::contextual_incident_leaf_keep_allows_path_change_but_conflicts_with_changed_leaf_both_directions ... ok
test semantic_review::source7_tests::reverse_operation_omission_gui_requires_raw_rationale_and_matches_one_core_batch_after_recovery ... ok
test semantic_review::source7_tests::semantic_operation_virtual_none_candidate_uses_exact_allocation_and_preserves_accepted_bytes ... ok
test semantic_review::ordered_tests::several_incoming_members_follow_source_order_despite_independent_identity_sorting ... ok
test semantic_review::tests::candidate_mixed_with_reference_take_uses_typed_correspondence_not_text_guessing ... ok
test semantic_review::tests::deselected_invalid_candidate_is_retained_but_does_not_poison_another_selected_scalar ... ok
test semantic_review::tests::explicit_trace_selection_updates_only_that_history ... ok
test semantic_review::tests::independent_review_clear_bound_ignores_retained_invalid_other_facet ... ok
test semantic_review::tests::independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity ... ok
test semantic_review::tests::first_equal_constructor_acknowledges_only_kind_and_allows_independent_leaves ... ok
test semantic_review::tests::inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approval ... ok
test semantic_review::tests::invalid_numeric_draft_and_protected_edits_retain_workflow_and_source ... ok
test semantic_review::tests::read_only_structured_facts_and_partial_rollup_are_honest ... ok
test semantic_review::tests::relation_candidates_preserve_endpoints_and_combine_selected_roles_and_bounds ... ok
test semantic_review::tests::scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers ... ok
test semantic_review::tests::scalar_keep_take_candidate_commit_exact_source_and_history_in_both_directions ... ok
test semantic_review::tests::state_kind_scalar_uses_typed_candidate_and_core_validation ... ok
test semantic_review::tests::stale_plan_and_source_writeback_failure_do_not_lose_decisions_or_history ... ok
test source_navigation::interaction::tests::captured_find_click_invalidates_without_app_message_and_preserves_focus ... ok
test source_navigation::interaction::tests::focus_rechecks_clock_between_inspection_and_application_and_missing_target_is_inert ... ok
test source_navigation::interaction::tests::raw_navigation_inputs_advance_but_passive_geometry_does_not_and_exhaustion_refuses ... ok
test source_navigation::query::tests::comments_strings_and_comment_gap_are_not_navigation_hits ... ok
test source_navigation::query::tests::detached_alias_and_identity_collisions_never_auto_select ... ok
test source_navigation::query::tests::dirty_but_parseable_text_has_current_names_and_no_validated_fallback ... ok
test source_navigation::query::tests::exact_multifile_unicode_crlf_definition_and_repeated_lexical_references ... ok
test semantic_review::view::reserved_artifact_tests::reserved_artifact_facet_and_reference_labels_survive_recovery_without_calling_it_deleted ... ok
test source_navigation::query::tests::exact_snapshot_readback_rejects_same_key_byte_membership_and_evidence_changes ... ok
test source_navigation::query::tests::invalid_include_syntax_never_borrows_the_old_reachable_file_set ... ok
test source_navigation::query::tests::include_cycle_depth_and_aggregate_edges_are_explicit_partial_outcomes ... ok
test source_navigation::query::tests::large_display_name_cannot_escape_the_owned_row_string_budget ... ok
test source_navigation::query::tests::keyword_alias_and_loaded_path_case_collisions_use_reviewed_index_rules ... ok
test source_navigation::query::tests::missing_include_partial_scope_does_not_resurrect_witness_text ... ok
test source_navigation::query::tests::omitted_rows_are_counted_before_result_allocation_and_never_confer_uniqueness ... ok
test source_navigation::query::tests::missing_and_noncanonical_authored_ids_remain_draft_spans_without_generated_anchors ... ok
test source_navigation::query::tests::owned_request_checks_raw_limits_immediately_and_extracts_only_on_worker ... ok
test source_navigation::query::tests::owned_request_moves_exact_text_into_worker_snapshot_and_matches_sync_results ... ok
test source_navigation::query::tests::owned_validation_proof_preserves_full_witness_count_and_partial_scope ... ok
test source_navigation::query::tests::raw_aggregate_limits_refuse_before_outline_or_copied_snapshot ... ok
test source_navigation::query::tests::sole_detached_candidate_and_unapplied_equal_bytes_require_choice ... ok
test source_navigation::query::tests::token_declaration_and_live_reference_limits_are_aggregate_not_per_file ... ok
test source_navigation::symbols::tests::admitted_unicode_path_budget_keeps_original_policy_after_single_fold ... ok
test source_navigation::query::tests::validation_witness_must_match_entry_and_complete_included_file_set ... ok
test source_navigation::symbols::tests::comment_separated_reference_keeps_crlf_unicode_and_disjoint_hit_spans ... ok
test source_navigation::symbols::tests::detached_uuid_clone_disables_anchor_even_for_included_query ... ok
test source_navigation::symbols::tests::declaration_order_and_duplicate_roles_refuse_instead_of_corrupting_hit_search ... ok
test source_navigation::symbols::tests::every_budget_admits_boundary_and_returns_no_index_one_below ... ok
test source_navigation::symbols::tests::duplicate_aliases_are_candidates_without_false_reference_attribution ... ok
test source_navigation::symbols::tests::incomplete_draft_is_lexical_and_error_token_does_not_leak_references ... ok
test source_navigation::symbols::tests::header_metadata_must_match_current_token_boundaries_and_authored_id ... ok
test source_navigation::symbols::tests::legal_keyword_classified_alias_is_not_discarded ... ok
test source_navigation::symbols::tests::missing_ids_produce_only_current_snapshot_locations_without_stabilization ... ok
test source_navigation::symbols::tests::noncanonical_header_identity_cannot_enter_through_query_metadata ... ok
test source_navigation::symbols::tests::nested_declaration_extents_do_not_turn_body_text_into_definition_hits ... ok
test source_navigation::symbols::tests::path_identity_rejects_normalization_and_case_collisions ... ok
test source_navigation::symbols::tests::query_header_admission_preserves_uppercase_and_json_escape_bytes ... ok
test source_navigation::symbols::tests::repeated_live_uses_are_distinct_and_strings_comments_history_are_excluded ... ok
test source_navigation::symbols::tests::reused_alias_cannot_steal_old_identity_and_old_locations_are_stale ... ok
test source_navigation::symbols::tests::stopped_outline_is_not_a_successful_empty_query ... ok
test source_navigation::symbols::tests::scope_provenance_and_detached_alias_collision_are_explicit ... ok
test source_navigation::symbols::tests::truncated_or_gapped_tokens_refuse_even_when_project_scope_is_partial ... ok
test source_navigation::symbols::tests::unicode_path_collisions_match_core_lowercase_policy ... ok
test source_navigation::tests::search_uses_utf8_columns_wraps_and_refreshes_after_edits ... ok
test source_tree::tests::active_scroll_offset_uses_the_visible_order_and_retains_ancestor_context ... ok
test source_tree::tests::collapsed_folders_retain_active_dirty_and_diagnostic_markers ... ok
test source_tree::tests::detached_buffers_keep_paths_and_only_clean_buffers_are_closable ... ok
test source_tree::tests::exact_unicode_spaces_and_file_folder_name_collisions_are_not_normalized ... ok
test source_tree::tests::deep_paths_build_expand_and_drop_on_a_small_stack ... ok
test source_tree::tests::folders_are_compact_and_sorted_before_files_without_losing_duplicate_basenames ... ok
test source_workspace::tests::draft_history_budget_covers_inactive_files_without_discarding_drafts ... ok
test source_workspace::tests::fresh_session_cannot_reuse_a_retained_buffer_generation ... ok
test source_workspace::tests::navigation_revision_wrap_cannot_reuse_an_old_workspace_key ... ok
test source_workspace::tests::navigation_scope_changes_invalidate_without_changing_unchanged_positions ... ok
test source_workspace::tests::navigation_versions_distinguish_reopened_recovered_and_changed_drafts ... ok
test tests::actor_port_creation_updates_containment_and_locate_reveals_its_owner ... ok
test tests::adding_bend_between_close_ports_stays_outside_owner_clearance ... ok
test source_workspace::tests::switching_retains_invalid_drafts_and_independent_history ... ok
test tests::bidirectional_transition_ui_commits_once_and_preserves_other_diagrams ... ok
test tests::child_work_does_not_invent_unallocated_or_mixed_performers ... ok
test tests::child_work_preserves_allocated_context_and_atomic_history ... ok
test tests::behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_views ... ok
test tests::counterpart_creation_focuses_visible_target_and_reuse_keeps_views ... ok
test tests::child_work_transfers_leaf_roles_without_fabricating_responsibilities ... ok
test tests::dragging_keeps_semantic_selection_and_same_node_remains_editable ... ok
test tests::escaped_string_mode_preserves_exact_control_characters ... ok
test tests::creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work ... ok
test tests::explicit_relationship_builder_never_mutates_on_preview ... ok
test tests::explorer_and_creation_selection_follow_their_own_placement ... ok
test tests::fit_adapts_to_canvas_returning_after_help_but_preserves_manual_navigation ... ok
test tests::fit_uses_measured_canvas_bounds_and_preserves_them_on_open ... ok
test tests::focused_explorer_follows_reparenting_without_changing_identity ... ok
test tests::graphical_rename_keeps_aliases_and_references_and_undo ... ok
test tests::identity_property_change_is_atomic_and_rewrites_all_live_references ... ok
test tests::invalid_numeric_draft_survives_navigation_save_and_help ... ok
test tests::invalid_save_and_continue_returns_to_repairable_draft ... ok
test tests::every_demo_record_has_a_lossless_graphical_draft ... ok
test tests::malformed_source_preserves_model_and_is_recoverable ... ok
test tests::mission_capability_authoring_is_contextual_atomic_and_text_equivalent ... ok
test tests::modal_owns_background_commands_and_shortcuts ... ok
test tests::manual_trace_requires_target_and_selection_normalizes_creation_kind ... ok
test tests::nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state ... ok
test tests::opening_and_creating_models_preserve_the_actual_window_size_for_fit ... ok
test tests::opening_legacy_source_surfaces_report_without_modifying_original_bytes ... ok
test tests::pending_source_prevents_property_draft_collision_and_preserves_both_if_present ... ok
test tests::placement_relocation_follows_cross_layer_traceability_view ... ok
test tests::placement_identity_edit_then_drag_and_diagram_identity_then_arrange ... ok
test tests::property_enum_and_optional_reference_roundtrip ... ok
test tests::reopening_canvas_for_a_modal_preserves_viewport_after_route_edits ... ok
test tests::routing_actions_are_atomic_guarded_and_fit_the_authored_detour ... ok
test tests::revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner ... ok
test tests::source_find_navigates_an_invalid_draft_without_applying_or_losing_it ... ok
test tests::source_navigation_waits_for_editor_layout_and_cancels_on_edit ... ok
test tests::system_only_models_open_the_populated_perspective ... ok
test tests::transition_resolutions_belong_to_the_effective_target_and_kind ... ok
test tests::selecting_current_constructors_preserves_payloads ... ok
test view_copy::tests::duplicate_state_view_preserves_machine_nesting_and_collapse_with_unique_aliases ... ok
test view_copy::tests::duplicate_retains_ports_routes_options_and_artifact_claims_with_exact_undo_redo ... ok
test view_copy::tests::missing_view_does_not_propose_an_empty_replacement ... ok
test views::source_tests::source_editor_windows_menu_payload_is_inert_but_space_and_altgr_still_edit ... ok
test views::source_tests::source_header_remains_scrollable_and_preserves_offset_across_diagnostic_refresh ... ok
test views::source_tests::source_header_keeps_editor_bounds_stable_through_lint_and_find_rebuilds ... ok
test views::tests::short_preserves_ascii_cjk_and_whitespace_policy ... ok
test views::tests::short_never_splits_retained_extended_graphemes ... ok
test views::tests::short_preserves_native_lifeline_clusters_at_cutoff ... ok
test views::tests::short_preserves_empty_and_small_limit_policy ... ok
test views::traceability::tests::traceability_filter_reaches_artifacts_after_68_traces_and_searches_all_metadata ... ok
test workflow::communication::tests::communication_requires_two_explicit_ports_and_recovers_the_partial_choice ... ok
test views::traceability::tests::traceability_browsing_retains_invalid_property_draft_and_existing_select_guard ... ok
test workflow::port_authoring_tests::boundary_picker_recomputes_for_unoriented_reverse_endpoints_without_relaxing_contracts ... ok
test workflow::port_authoring_tests::duplicate_port_names_show_owner_contract_and_unique_alias ... ok
test workflow::port_authoring_tests::fresh_functions_are_actual_picker_targets_and_plan_enables_apply ... ok
test workflow::port_authoring_tests::generic_component_port_connection_keeps_its_endpoint_and_direction ... ok
test workflow::port_authoring_tests::ordinary_standard_connections_default_unoriented_and_new_roles_exclude_legacy ... ok
test workflow::recovery::tests::incomplete_relationship_and_trace_recover_raw_fields_and_reject_changed_model ... ok
test workflow::recovery::tests::transition_recovery_replans_typed_request_without_persisted_commands ... ok
test workflow::transition_tests::basic_transition_boundary_choice_is_required_without_an_inferred_system ... ok
test workflow::transition_tests::basic_transition_creates_overview_when_existing_views_cannot_show_result ... ok
test workflow::transition_tests::basic_transition_explicit_destination_appends_below_existing_bounds_and_is_one_source_edit ... ok
test workflow::transition_tests::basic_transition_invalid_explicit_destination_retains_choice_and_refuses ... ok
test workflow::transition_tests::basic_transition_reconcile_keeps_existing_selected_view_placement_and_route_exact ... ok
test workflow::transition_tests::basic_transition_skips_lowest_uuid_state_view_and_preserves_oa ... ok
test workflow::port_authoring_tests::boundary_picker_matches_exact_commands_across_contracts_and_ownership ... ok
test result: ok. 894 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1137.48s
@@ -0,0 +1,15 @@
# Corrected navigation: Windows desktop test result
Tested source: `46b4f3c0d0f04050ad6d705051ee0199138e47a2` on `codex/diagram-coverage-fixes-ytr83r-20260912`.
`cargo test -p oa-desktop --all-targets --locked --offline` completed with **894 passed, 0 failed, 1 ignored**, exit 0. Compilation reported 12m44s; execution reported 1137.48 seconds. [Raw output](correction-tests.log), [receipt](correction-tests-receipt.json), and [all 562 verified compiler inputs](correction-all-inputs.json) are preserved. Each input matched its pre-check hash and byte length after the desktop run. The receipt conservatively records uncommitted work: the checkout also contained an unwired, untracked experiment directory, which was not included by this build. The compiled correction source matches the published commit.
All three failures of the [older source](README.md) now pass. The passing run includes the new exact qualified-preview link checks, independent Allocation grouping fixtures, port/state semantic-selection and visual-focus checks, stale chooser/deferred viewport cases, camera persistence, and existing desktop/source/history regressions.
The one ignored case is `app::source_symbols::tests::source_symbols_100_modules_1000_definitions_10000_uses`, explicitly a manual latency/process-memory trial intended to run alone. This result does not establish its performance threshold or large-model native responsiveness. The host also ran an independent core compilation and Capella startup during portions of execution; elapsed time here is not a comparative performance benchmark.
Core-specific outline tests and strict lint are separate gates. A matching GUI build and native acceptance of the corrected source remain pending. The process/scenario free-editing requirements are still unfinished, and Linux/WSL remains paused by the user.
## Raw-log preservation
Post-push blob verification detected that Git normalized CRLF line endings in the initially published raw log and receipt at `0eca66f`. A follow-up commit disables text conversion for these two exact evidence paths and restores their original bytes, including the terminal blank line. Historical commits are unchanged. The unrestricted Git whitespace check reports that blank line at `correction-tests.log:910`; it is an intentional raw-evidence exception. The same check excluding this exact raw log passes for the report, receipt and manifest changes. This does not change the successful source whitespace check recorded in the correction handoff.
@@ -0,0 +1,11 @@
# Exact archived source/provenance/receipt bytes; do not normalize line endings.
/before-details-fix/crates/oa-desktop/src/app/source_rename.rs -text whitespace=cr-at-eol
/before-details-fix/crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs -text whitespace=cr-at-eol
/before-details-fix/manifest.json -text whitespace=cr-at-eol
/details-fix-hashes.json -text whitespace=cr-at-eol
/final-source-hashes.json -text whitespace=cr-at-eol
/formatting.json -text whitespace=cr-at-eol
/import-provenance.json -text whitespace=cr-at-eol
/initial-source-hashes.json -text whitespace=cr-at-eol
/pre-format-source-hashes.json -text whitespace=cr-at-eol
/artifacts.json -text whitespace=cr-at-eol
@@ -0,0 +1,75 @@
# Selective navigation and Text alias rename union
This is prepared source and static review evidence. **The combined union has not been compiled, tested or exercised natively.** Formatting and source-hash checks do not establish those results. The integration owner retains publication and runtime acceptance; peer-owned compact Text layout work is outside this change.
## Pinned inputs and dependency boundary
- Our base is [4db1ef543ced916179194272bf0e65dce73cb107](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/4db1ef543ced916179194272bf0e65dce73cb107), preserving the evidence bytes for navigation production [46b4f3c0d0f04050ad6d705051ee0199138e47a2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/46b4f3c0d0f04050ad6d705051ee0199138e47a2).
- Peer rename source is [cbc49a7802fb1bba0dd284fd904c700a0703ae70](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/cbc49a7802fb1bba0dd284fd904c700a0703ae70). Its production delta is [dfb6102ba5aec9e26a48cc5472b51a4b349459fd](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/dfb6102ba5aec9e26a48cc5472b51a4b349459fd) plus the mandatory `cbc49a7` scroll correction, against [be7a8cd8d7d63aa18e0196bbd36f9e646764c761](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/be7a8cd8d7d63aa18e0196bbd36f9e646764c761). Intervening `4686d05` contains evidence, not production dependencies.
- [Import provenance](import-provenance.json) records all 24 peer files and 10 corresponding baseline files. All 34 raw downloads were checked against published Gitea Git blob hashes; SHA-256 and sizes are retained.
The union changes 27 source/documentation paths: 25 Rust files and two language documents. Document/query prerequisites were identical to the peer baseline; 14 new files were added and shared App/view/documentation hunks were merged selectively. The entire core tree and the existing `examples/release1` timing fixture match the peer. No core/schema, dependency manifests, lockfile, allocation/Delete semantics, user specification or historical evidence was replaced. Existing camera, lifecycle, repair, exact navigation focus and grapheme behavior remains in the integration base.
## Source review findings and corrections
These findings concern the combination of pinned branches. They do not retroactively assert defects in either branch's isolated acceptance evidence. The initial boundaries were published in [issue 17 comment 717](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/17#issuecomment-717).
| Finding | Correction and evaluator |
| --- | --- |
| Diagram usage dispatch precedes generic modal handling, so merely adding a Rename modal gate permits queued diagram Open over Rename. | Both admissions now exclude the other chooser; source navigation and Rename retain reference chooser exclusions. Real App tests cover Rename with an existing approval and both loading/ready diagram choosers. |
| A Rename completion arriving after Close and diagram navigation can cancel the newer deferred camera/focus during outer reconciliation. Worker slots also survive document replacement. | Completion is passive in reveal reconciliation and releases only its exact slot before modal gates. Tests cover target/preview, forged/duplicate completion, first measurement, and all three real outstanding workers across reset. |
| Rejected old Close/Confirm must be inert before outer source/camera reconciliation. Captured Begin and Undo Rename have an additional queued-intent boundary. | Envelope checks run before reconciliation. Begin still requires Text; Undo Rename still requires its existing live-context predicate. Tests retain a real old approval and exercise stale Begin/Close/Confirm and queued Undo Rename after diagram navigation. Ordinary model Undo/Redo preserves the exact accepted snapshot stack. |
| Independent review found a P2 Details/Preview visit gap: a held File-navigation snap could target the same preview identity after returning from Details and erase later manual scrolling. | An actual Details visibility change rotates the visit UUID without changing file/hunk/line/segment or scheduling a scroll reset. Repeated same-value visibility is inert. The retained-widget test exercises held reset → Details → Preview → manual scroll → held reset, including both mounted and between-frame Details transitions. |
The integration owner reported an independent static review approving the final Details correction, with both corrected pre-format hashes matching [details-fix-hashes.json](details-fix-hashes.json), and no further finding. This was source review, not test/native evidence. Formatting subsequently changes presentation only; the final manifest identifies the resulting inputs for future gates.
The existing `preview_destination` UUID remains paired in the mounted widget and every page/approval scroll operation. The Details correction does not force a return to the top. If Details is actually mounted, the returning preview follows Iced's ordinary widget-state lifecycle; if both switches happen between frames, the retained scroll state remains subject to the existing contract. In either case, an older operation cannot address the new visit.
## Exact source and artifact records
- [Initial frozen union hashes](initial-source-hashes.json): the initial 27-path handoff before the Details correction.
- [Reviewed Details fix hashes](details-fix-hashes.json): the two changed files before formatting.
- [Pre-format hashes](pre-format-source-hashes.json): all 27 inputs after the accepted Details correction.
- [Final source hashes](final-source-hashes.json): all 27 inputs after bounded formatting; use these to identify the next compiler inputs.
- [Formatting receipt](formatting.json): formatter version, exact relative argument list, changed paths, check exit and checks against tracked inputs outside the owned set.
- [Raw artifact hashes](artifacts.json) and [scoped attributes](.gitattributes): exact archived bytes and text-conversion policy.
The two independently archived pre-fix Rust files are [source_rename.rs](before-details-fix/crates/oa-desktop/src/app/source_rename.rs) and [preview_scroll_tests.rs](before-details-fix/crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs). Their [original manifest](before-details-fix/manifest.json) is copied unchanged. These copies intentionally retain the reviewed old behavior and must not be compiled as current sources or reformatted. Exact `-text` entries preserve their raw bytes and the checksum-bound receipts. The current implementation is in [the App rename controller](../../../crates/oa-desktop/src/app/source_rename.rs).
Formatting used the local `rustfmt` binary with edition 2024 and `skip_children=true` over only the 25 named Rust files. The preceding leaf-only `--check` found expected formatting differences; the subsequent all-owned-file format/check receipt records completion. No Cargo command, compiler, test, native UI or staging/publication operation was performed during this preparation.
## Evaluators to run
The six combined App tests use a parsed four-file Unicode/CRLF fixture with an explicitly identified distant placement. They dispatch actual App events, produce worker completions independently without polling the returned workers, and check whole model/source, accepted snapshot identity, history, semantic/visual selection and actual deferred viewport measurements.
The four retained-widget tests execute real scroll operations on the production dialog widget tree with a CPU font/layout renderer. They do not establish native GPU paint or package behavior.
These group filters must select **six** and **four** tests respectively; do not substitute filename filters:
```text
cargo test --locked --offline -p oa-desktop app::source_rename::tests::navigation_union_tests:: -- --test-threads=1
cargo test --locked --offline -p oa-desktop app::source_rename::tests::preview_scroll_tests:: -- --test-threads=1
```
Exact individual filters:
```text
app::source_rename::tests::navigation_union_tests::rename_modal_rejects_queued_diagram_and_source_navigation_without_losing_approval
app::source_rename::tests::navigation_union_tests::loading_and_ready_diagram_choosers_reject_a_previously_captured_rename_begin
app::source_rename::tests::navigation_union_tests::late_target_and_preview_results_release_only_their_slot_and_preserve_first_camera_measurement
app::source_rename::tests::navigation_union_tests::stale_begin_close_and_actual_confirm_leave_new_diagram_focus_and_pending_measurement_unchanged
app::source_rename::tests::navigation_union_tests::document_reset_retains_all_three_worker_slots_until_their_exact_results_arrive
app::source_rename::tests::navigation_union_tests::rename_then_exact_caret_diagram_navigation_preserves_complete_model_undo_redo_history
app::source_rename::tests::preview_scroll_tests::explicit_navigation_resets_retained_scroll_but_id_change_alone_does_not
app::source_rename::tests::preview_scroll_tests::delayed_page_and_approval_operations_cannot_reset_a_new_destination
app::source_rename::tests::preview_scroll_tests::repeated_page_visits_filters_and_fast_preview_completion_have_distinct_destinations
app::source_rename::tests::preview_scroll_tests::details_round_trip_retires_held_reset_without_changing_page_or_repeated_visibility_scroll
```
Additional relevant groups are `source_rename`, `document::alias_rename::tests::`, `app::diagram_usages::tests::`, and `app::diagram_reveal::tests::`. The existing isolated timing filters are `accepted_release1_prepare_commit_sync_and_confirm_timing` and `accepted_release1_constructor_vs_reuse_through_next_highlight`, each run alone with `--ignored --nocapture --test-threads=1` when the integration owner schedules those trials.
Compilation, focused/full regression gates, strict lint, matching build and native navigation/rename/Save/Undo/Redo/package acceptance remain open. The peer's published source/native/package evidence remains tied to its own identified source and executable, not this union. The existing permanent Text layout is retained; compact command/layout work remains with the peer's separate scope.
The [prepared native acceptance workflows](native-acceptance.md) cover preview visibility/scroll, mutual modal input ownership, immediate keyboard editing, diagram navigation and complete source/disk/history restoration. They have not been executed on this union.
Parent staging also checks added files. Its initial cached whitespace check identified original CRLF bytes in three checksum-bound JSON artifacts; exact raw-artifact attributes now allow CR at line endings while retaining ordinary whitespace checks. No archived bytes or source were changed by this correction. The earlier working-tree diff check did not cover those then-untracked artifacts.
@@ -0,0 +1,59 @@
{
"files": [
{
"path": "before-details-fix/crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45315,
"sha256": "7b13c39d4c1509d13aef2de4580b9e418cf7ac1ae87d42bd21dcd34ecc70f863",
"git_blob": "eee376e2461477a4ac123c849fb165d9e2867b2b"
},
{
"path": "before-details-fix/crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 10937,
"sha256": "06c8dc66b696f70427a190cd174be373a8114bd2f49b6e8f1152173d59e9ec29",
"git_blob": "4641cf7f8db309b12c7bd668dd4ea771cb456fa6"
},
{
"path": "before-details-fix/manifest.json",
"bytes": 595,
"sha256": "4ac41b15970e9525b5def9af9eb0d57eaa9d3919dd2dd6fe9c4929a60cb17aa6",
"git_blob": "4507b73945f1660a9c1e9a0ea3064d1d40b7b764"
},
{
"path": "details-fix-hashes.json",
"bytes": 494,
"sha256": "275a692bda781a561c0a50c9af1900c21db42327366daed5ec54a6ade2c1b810",
"git_blob": "da4aa80d6ab9cb752fc398337f2e12a63a10d07c"
},
{
"path": "final-source-hashes.json",
"bytes": 6679,
"sha256": "a1c007d3d44da805b840423ea6950811c1b43dc780c753164320105fb1296f0b",
"git_blob": "616cc048be1c7bee62bd12e73a493c2a7b57ff20"
},
{
"path": "formatting.json",
"bytes": 1986,
"sha256": "14b0812cb176296f07fa7712dfc9fc5e5a9cc94ec10a4038961e469ab0650647",
"git_blob": "d0ca466fbaa24abf1270c56e3cfe3842458458c4"
},
{
"path": "import-provenance.json",
"bytes": 10539,
"sha256": "9761b644ca7c5a3cbcc543834848170c3baa27c6f80093c68a8f64d0c3677b30",
"git_blob": "1fe1da89a3ddf54af931ac9f3c9e3d537fd422fd"
},
{
"path": "initial-source-hashes.json",
"bytes": 7723,
"sha256": "fbc1109e039150265dbdc1d3c7ffda2279b7323468488732c776f67515b78e66",
"git_blob": "291589c30276eb7f7000daa8ee01f3feb613b531"
},
{
"path": "pre-format-source-hashes.json",
"bytes": 6679,
"sha256": "d49583b87bb12655fd96c17c1629b832b6389aeddf98cc986ad97c7c19282f13",
"git_blob": "e746e7a36318fce9ddf607122006f2088343d89b"
}
],
"note": "Raw archived inputs and receipts. This manifest and .gitattributes intentionally do not hash themselves."
}
@@ -0,0 +1,310 @@
//! Execute the controller's scroll operations on a retained, scrolled dialog.
use super::*;
use crate::views::source_tests::FontLayout;
use iced::advanced::{Layout, layout, widget};
use iced::futures::StreamExt;
use iced::{Rectangle, Size, Vector};
const HOST: Size = Size::new(720., 660.);
fn paged_fixture() -> App {
let mut source = bundle();
let name = "Long engineering name 雨 ".repeat(18);
source.files.get_mut("defs.arcadia").unwrap().replace_range(
..,
&format!(
"oa_activity pump \"{name}\" id \"00000000-0000-0000-0000-000000000002\" {{}}\r\n\
oa_activity backup \"Backup\" id \"00000000-0000-0000-0000-000000000004\" {{}}\r\n"
),
);
let mut uses = String::new();
for index in 0..16 {
if index == 12 {
uses.push_str(&"// unchanged gap\r\n".repeat(10));
}
uses.push_str(&format!(
"interaction use_{index} \"{name}\" id \"00000000-0000-0000-0001-{index:012}\" {{ source @pump; target @backup; }}\r\n"
));
}
source.files.insert("uses.arcadia".into(), uses);
let mut app = fixture();
app.window_size = HOST;
app.document = Document::from_bundle(source, LIMITS).unwrap();
app.source_workspace = Workspace::new(app.document.bundle(), true);
app.source_workspace.active = "uses.arcadia".into();
app.show_source_file(false);
let start = app.source_workspace.text().find("source @pump").unwrap() + "source @".len();
app.source_editor
.move_to(cursor(app.source_workspace.text(), start, start + 4));
begin(&mut app);
input(&mut app, "pump_long");
let _ = preview(&mut app);
app
}
fn scroll_id(app: &App) -> widget::Id {
let form = app.source_rename.form.as_ref().unwrap();
preview_scroll_id(form, form.approval.as_ref().unwrap().id)
}
struct Geometry {
id: widget::Id,
offset: Option<Vector>,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
_: Rectangle,
_: Rectangle,
offset: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == Some(&self.id) {
self.offset = Some(offset);
}
}
}
struct Mounted {
editor: text_editor::Content<FontLayout>,
tree: widget::Tree,
}
impl Mounted {
fn new(app: &App) -> Self {
let editor = text_editor::Content::<FontLayout>::new();
let tree = widget::Tree::new(dialog_content(app, &editor).as_widget());
Self { editor, tree }
}
fn operate(&mut self, app: &App, operation: &mut dyn widget::Operation) {
let mut view = dialog_content(app, &self.editor);
// Preserve the actual scroll state across the new page's view diff.
self.tree.diff(view.as_widget());
let node = view.as_widget_mut().layout(
&mut self.tree,
&FontLayout,
&layout::Limits::new(Size::ZERO, HOST),
);
view.as_widget_mut()
.operate(&mut self.tree, Layout::new(&node), &FontLayout, operation);
}
fn offset(&mut self, app: &App) -> f32 {
let mut geometry = Geometry {
id: scroll_id(app),
offset: None,
};
self.operate(app, &mut geometry);
geometry.offset.expect("mounted preview scrollable").y
}
fn scroll_down(&mut self, app: &App) -> f32 {
self.operate(
app,
&mut widget::operation::scrollable::scroll_to(
scroll_id(app),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(10_000.),
},
),
);
let offset = self.offset(app);
assert!(offset > 10., "fixture must really be scrolled");
offset
}
async fn execute(&mut self, app: &App, task: Task<Message>) -> usize {
let mut count = 0;
if let Some(mut actions) = iced_runtime::task::into_stream(task) {
while let Some(action) = actions.next().await {
let iced_runtime::Action::Widget(mut operation) = action else {
panic!("preview paging must only schedule a widget operation");
};
self.operate(app, operation.as_mut());
count += 1;
}
}
count
}
}
#[tokio::test]
async fn explicit_navigation_resets_retained_scroll_but_id_change_alone_does_not() {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
mounted.scroll_down(&app);
let old_id = scroll_id(&app);
let task = app.update(Message::SourceRename(Event::File(
invocation, approval, true,
)));
assert_ne!(old_id, scroll_id(&app));
// Negative control: even after diff/layout with the new id and new file,
// Iced retains the previous offset until the returned operation executes.
assert!(mounted.offset(&app) > 10.);
assert_eq!(mounted.execute(&app, task).await, 1);
assert_eq!(mounted.offset(&app), 0.);
for navigation in [
Event::Line(invocation, approval, true),
Event::Segment(invocation, approval, true),
Event::Hunk(invocation, approval, true),
Event::File(invocation, approval, false),
Event::Search(invocation, "uses".into()),
Event::Search(invocation, "no-path-match".into()),
Event::Search(invocation, String::new()),
] {
mounted.scroll_down(&app);
let task = app.update(Message::SourceRename(navigation));
assert_eq!(mounted.execute(&app, task).await, 1);
assert_eq!(mounted.offset(&app), 0.);
assert_eq!(evidence(&app), before);
}
// Passive redraw and unchanged filter input preserve deliberate scrolling.
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.offset(&app), offset);
let task = app.update(Message::SourceRename(Event::Search(
invocation,
String::new(),
)));
assert_eq!(mounted.execute(&app, task).await, 0);
assert_eq!(mounted.offset(&app), offset);
let task = app.update(Message::SourceRename(Event::Input(
invocation,
"pump_long".into(),
)));
assert_eq!(mounted.execute(&app, task).await, 0);
assert_eq!(mounted.offset(&app), offset);
let task = app.update(Message::SourceRename(Event::File(
invocation, approval, false,
)));
assert_eq!(
mounted.execute(&app, task).await,
0,
"clamped first-page request is a no-op"
);
assert_eq!(mounted.offset(&app), offset);
assert_eq!(evidence(&app), before);
}
#[tokio::test]
async fn delayed_page_and_approval_operations_cannot_reset_a_new_destination() {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let old_approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
let old_page_task = app.update(Message::SourceRename(Event::File(
invocation,
old_approval,
true,
)));
let next_page_task = app.update(Message::SourceRename(Event::Line(
invocation,
old_approval,
true,
)));
assert_eq!(mounted.execute(&app, next_page_task).await, 1);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, old_page_task).await, 1);
assert_eq!(mounted.offset(&app), offset, "old page target is absent");
let old_approval_task = app.update(Message::SourceRename(Event::Search(
invocation,
"defs".into(),
)));
input(&mut app, "pump_new");
let _ = preview(&mut app); // Real new approval in the same invocation/page.
assert_ne!(
app.source_rename
.form
.as_ref()
.unwrap()
.approval
.as_ref()
.unwrap()
.id,
old_approval
);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, old_approval_task).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"old approval target is absent"
);
let task = app.update(Message::SourceRename(Event::File(
invocation,
old_approval,
true,
)));
assert_eq!(
mounted.execute(&app, task).await,
0,
"stale envelope is rejected before scheduling"
);
assert_eq!(mounted.offset(&app), offset);
assert_eq!(evidence(&app), before);
}
#[tokio::test]
async fn repeated_page_visits_filters_and_fast_preview_completion_have_distinct_destinations() {
let mut app = paged_fixture();
let before = evidence(&app);
let form = app.source_rename.form.as_ref().unwrap();
let invocation = form.invocation;
let approval = form.approval.as_ref().unwrap().id;
let mut mounted = Mounted::new(&app);
let first_visit = app.update(Message::SourceRename(Event::File(
invocation, approval, true,
)));
let next_page = app.update(Message::SourceRename(Event::Line(
invocation, approval, true,
)));
assert_eq!(mounted.execute(&app, next_page).await, 1);
let return_visit = app.update(Message::SourceRename(Event::Line(
invocation, approval, false,
)));
assert_eq!(mounted.execute(&app, return_visit).await, 1);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, first_visit).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"old visit to the identical page is stale"
);
let old_filter = app.update(Message::SourceRename(Event::Search(
invocation,
"uses".into(),
)));
let new_filter = app.update(Message::SourceRename(Event::Search(
invocation,
"us".into(),
)));
assert_eq!(mounted.execute(&app, new_filter).await, 1);
let offset = mounted.scroll_down(&app);
assert_eq!(mounted.execute(&app, old_filter).await, 1);
assert_eq!(
mounted.offset(&app),
offset,
"old filter on the same page is stale"
);
input(&mut app, "pump_new");
let job = start_preview(&mut app);
let completion = preview_completion(&app, job);
// No intermediate frame mounts the temporary no-approval view. The old
// Scrollable survives even though the new approval has a different id.
let ready = app.update(Message::SourceRenameReady(completion));
assert!(mounted.offset(&app) > 10.);
assert_eq!(mounted.execute(&app, ready).await, 1);
assert_eq!(mounted.offset(&app), 0.);
assert_eq!(evidence(&app), before);
}
@@ -0,0 +1,17 @@
{
"base": "4db1ef543ced916179194272bf0e65dce73cb107",
"peer": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"status": "Pre-fix union; source-reviewed, uncompiled, Details delayed-scroll finding",
"files": [
{
"path": "crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45315,
"sha256": "7b13c39d4c1509d13aef2de4580b9e418cf7ac1ae87d42bd21dcd34ecc70f863"
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 10937,
"sha256": "06c8dc66b696f70427a190cd174be373a8114bd2f49b6e8f1152173d59e9ec29"
}
]
}
@@ -0,0 +1,14 @@
[
{
"path": "crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45705,
"sha256": "4714cde897a7a365958a6014f6a546df682a963a48c2fd9d918ddb3c5500268d",
"git_blob": "a4e41d1a3ccd8c2a37e763c3ab50ff6cac8935af"
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 13821,
"sha256": "ca8d49b5de9e4b848d73cc4c0a4884c0a006a48c3e171f32fb496eac4d6aa180",
"git_blob": "3025e756299bab0c0c48aa236e99dd9788e557ee"
}
]
@@ -0,0 +1,168 @@
{
"base": "4db1ef543ced916179194272bf0e65dce73cb107",
"peer": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"files": [
{
"path": "crates/oa-desktop/src/app.rs",
"bytes": 233137,
"sha256": "c4af6a3bce725ed52e1e5919a2177a12c603d9beb6f0f3485925269171bef7ac",
"git_blob": "8b1edac34f9b8b96df76d4fd759bd6aaae5c0a5e"
},
{
"path": "crates/oa-desktop/src/app/diagram_reveal.rs",
"bytes": 5996,
"sha256": "6d94b32ae64aaebdf81b80d5b8f69a5077425d51727c736a57ffb5f334b3b76a",
"git_blob": "ab4dd765b1b03556e9e7f5abf4ca4b5ae8692c1d"
},
{
"path": "crates/oa-desktop/src/app/diagram_usages.rs",
"bytes": 18131,
"sha256": "ae7ae6f24a845549d97715d0229861bd49dc0f82d4ce0632f1c134a73de605ee",
"git_blob": "fb17862f667b597f76d77c7c320f1b0b7935758c"
},
{
"path": "crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45621,
"sha256": "129147bd71286f482c3958a0f5011b9f9d11b6fce72b7ae843cc9e6362670721",
"git_blob": "7bcbaa0adaf143e9e37ea5071acf5924b415c0e5"
},
{
"path": "crates/oa-desktop/src/app/source_rename/alias_input.rs",
"bytes": 6940,
"sha256": "5888b50e4d6fabd8cd0c6d70986a791d8e95f190453ea37ebb251f2fa19cda90",
"git_blob": "f7240271ea87a957a686249bf629bc0c36e924fe"
},
{
"path": "crates/oa-desktop/src/app/source_rename/alias_input/tests.rs",
"bytes": 7279,
"sha256": "f9dd619c440a6d784792be5e41bad562ef312e07967d193b0e96cdc127debfb2",
"git_blob": "579880f4f6fab0949805452451b70bdf9e43ac1e"
},
{
"path": "crates/oa-desktop/src/app/source_rename/editor_patch.rs",
"bytes": 11423,
"sha256": "049c9774c1ca28cda01f9a8e024604bef282366aa52c53c17c1918dbd09c3441",
"git_blob": "f30a08b8e712d3f59aefa8dc478c2db8e46512d1"
},
{
"path": "crates/oa-desktop/src/app/source_rename/navigation_union_tests.rs",
"bytes": 17106,
"sha256": "239bae866176409906140233d9e906ae8a6d9bf810cdb5503b95b01d5eae327e",
"git_blob": "e208548be6e06ca7011bc9ee53921c52f6631092"
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_glyph_tests.rs",
"bytes": 16247,
"sha256": "d45a799bd97047c6b77ff5b18334f2a71912b65d5239f2b8ce7017588ccff0f4",
"git_blob": "90b3864106072a2fb39c2e1f88a877465f2dc859"
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 14051,
"sha256": "62354e03219c75b4e24dad7bae7c31486cb4fda06059493f392e381f52700d22",
"git_blob": "4adb3bf470195047a95607ad40f098adad21da1d"
},
{
"path": "crates/oa-desktop/src/app/source_rename/tests.rs",
"bytes": 54012,
"sha256": "90f69af589fd1698f20ab67299785f8b6c60a99c092e615b8ddb446feff8d5f0",
"git_blob": "2b90ec6e35c0d0a10fa05268a447a0bb53131bf0"
},
{
"path": "crates/oa-desktop/src/app/source_rename_layout_timing_tests.rs",
"bytes": 12253,
"sha256": "dadf8ce0ec989b9a370703a330bde4b415a813273917037fc6a732b9bf2f98ce",
"git_blob": "ea37c4d44a8c54f00f9508250666b5f4a55008ce"
},
{
"path": "crates/oa-desktop/src/app/source_rename_timing_tests.rs",
"bytes": 13912,
"sha256": "942753946d00cab9d4a19717bec5f429af58c070e2cd1b97cb4d32ed6fb93967",
"git_blob": "b7df39f71542b08ca55acc5c97d580378394f451"
},
{
"path": "crates/oa-desktop/src/app/source_symbols.rs",
"bytes": 21084,
"sha256": "43382383bc09c496a4262e7b16a01855e533c6203d046076c16032719bf7ae14",
"git_blob": "03aaade7cc7e660ab3ded9ab9920522b08f550a0"
},
{
"path": "crates/oa-desktop/src/document.rs",
"bytes": 32600,
"sha256": "e87d7ce456bcab1dab55249021eaa8056f41b888723d143428486e31cb9ca4b1",
"git_blob": "e68c312afbc17da193db832b022eabb6aeadaac6"
},
{
"path": "crates/oa-desktop/src/document_alias_rename.rs",
"bytes": 8021,
"sha256": "ce64d723a51a834699ebcf74d89d406a77b6ccc5419bcd3f63cf0e27e9fd25cb",
"git_blob": "aa98e63b9c81decea42746f1766128765da47e75"
},
{
"path": "crates/oa-desktop/src/document_alias_rename_tests.rs",
"bytes": 18027,
"sha256": "847dca1e50970d8a3d75c7e881754453e801f4f3d34f03a88410a334899783bc",
"git_blob": "5e773f46fb7ff50ebf1a43a71ee87225e8451389"
},
{
"path": "crates/oa-desktop/src/guide.rs",
"bytes": 120562,
"sha256": "8795295d86b51c5e6b589017445886132f6349c5aef0ac3542f202c1c6afb157",
"git_blob": "50a6c6c9ac64661890062f9f88343d8d52e0cef3"
},
{
"path": "crates/oa-desktop/src/main.rs",
"bytes": 1549,
"sha256": "2230e12ddbfda7e52fa36b04bf676ca822c7807f1d31715cd0527653679c889b",
"git_blob": "cb86659ddc6a9e18026d2fc3de5f67e77a575167"
},
{
"path": "crates/oa-desktop/src/source_navigation/query.rs",
"bytes": 36149,
"sha256": "4f6ff7d708bb99c64b7370a1761a6099d5f97be4b319582ab4faa7bb8aa42bb9",
"git_blob": "316224b35e4def678df49de62d411076e3e979ed"
},
{
"path": "crates/oa-desktop/src/source_navigation/query_tests.rs",
"bytes": 31577,
"sha256": "121f197dbae319f47ec59ff875733f1985c7607badb2e4c3d97ef8746d13afa9",
"git_blob": "0b6bd7e101dcc031647222c2c788e4be362b5510"
},
{
"path": "crates/oa-desktop/src/source_rename.rs",
"bytes": 116,
"sha256": "1ead2a66d9fd7429b8cfca1338b038e7dd97e21ae76cb722f3ca4c43a520ca00",
"git_blob": "f185672c488712aa50de495561062f90c014eaaf"
},
{
"path": "crates/oa-desktop/src/source_rename/origin.rs",
"bytes": 9275,
"sha256": "11f619310fe837f20a8d495b13e557cafe258270350815c6ac8c848f51cd76ac",
"git_blob": "3da6ccb36964f9456cbe258138838f1176bd1058"
},
{
"path": "crates/oa-desktop/src/source_rename/preview.rs",
"bytes": 19903,
"sha256": "bf96e31986d143fce43ad1634d7028f1225aebcd9013955ca2ba18cc4fd1190d",
"git_blob": "df96bb6d43e66c9f07f94410871809b3b371e5da"
},
{
"path": "crates/oa-desktop/src/views.rs",
"bytes": 71224,
"sha256": "a60dea8285f844ea386e3ff429fd47e559e2a288dc88d4d98eb0f95d3684f2b1",
"git_blob": "0ebc252ab42ce1281482b7d61c4f797755f20d7d"
},
{
"path": "docs/language.html",
"bytes": 184689,
"sha256": "b0eb7199e481d8ecc8dd04bf1e1951569b6971acb0f1d1263afb40a0ec6ba14f",
"git_blob": "fb55b2ff958a785c59a671389e5a852d8a572015"
},
{
"path": "docs/language.rst",
"bytes": 139785,
"sha256": "9009c564f20f833f7d545bd286ee2b5a18fa86fbf8c6a6e68d2ffdd287a27f0f",
"git_blob": "0116a562098e9d8a5fc885442fa57621ff63e01d"
}
]
}
@@ -0,0 +1,49 @@
{
"formatter": "rustfmt 1.9.0-stable (48a229ceae 2026-09-01)",
"command": [
"rustfmt",
"--edition",
"2024",
"--config",
"skip_children=true",
"crates/oa-desktop/src/app.rs",
"crates/oa-desktop/src/app/diagram_reveal.rs",
"crates/oa-desktop/src/app/diagram_usages.rs",
"crates/oa-desktop/src/app/source_rename.rs",
"crates/oa-desktop/src/app/source_rename/alias_input.rs",
"crates/oa-desktop/src/app/source_rename/alias_input/tests.rs",
"crates/oa-desktop/src/app/source_rename/editor_patch.rs",
"crates/oa-desktop/src/app/source_rename/navigation_union_tests.rs",
"crates/oa-desktop/src/app/source_rename/preview_glyph_tests.rs",
"crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"crates/oa-desktop/src/app/source_rename/tests.rs",
"crates/oa-desktop/src/app/source_rename_layout_timing_tests.rs",
"crates/oa-desktop/src/app/source_rename_timing_tests.rs",
"crates/oa-desktop/src/app/source_symbols.rs",
"crates/oa-desktop/src/document.rs",
"crates/oa-desktop/src/document_alias_rename.rs",
"crates/oa-desktop/src/document_alias_rename_tests.rs",
"crates/oa-desktop/src/guide.rs",
"crates/oa-desktop/src/main.rs",
"crates/oa-desktop/src/source_navigation/query.rs",
"crates/oa-desktop/src/source_navigation/query_tests.rs",
"crates/oa-desktop/src/source_rename.rs",
"crates/oa-desktop/src/source_rename/origin.rs",
"crates/oa-desktop/src/source_rename/preview.rs",
"crates/oa-desktop/src/views.rs"
],
"rust_inputs": 25,
"owned_inputs": 27,
"formatted_paths": [
"crates/oa-desktop/src/app/source_rename.rs",
"crates/oa-desktop/src/app/source_rename/navigation_union_tests.rs",
"crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs"
],
"outside_inputs_checked": 312,
"outside_inputs_unchanged": true,
"format_exit": 0,
"check_exit": 0,
"stdout": "",
"stderr": "",
"compilation_or_tests_run": false
}
@@ -0,0 +1,274 @@
[
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app.rs",
"bytes": 217694,
"git_blob": "b9d14f11bd36173053974f619ea6e616db8e5dc4",
"sha256": "d2e13b4527eec6c0920668d312d647806c762ed0a2f3ad9595a1243d9993fc14"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45005,
"git_blob": "8c0da85bc6f3a306931280452447eba962d7e66d",
"sha256": "b3402c661330da1bc50a6135ffbefbd7ab559c364e07f5c52f23ab9e3e7f6e74"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename/alias_input.rs",
"bytes": 6940,
"git_blob": "f7240271ea87a957a686249bf629bc0c36e924fe",
"sha256": "5888b50e4d6fabd8cd0c6d70986a791d8e95f190453ea37ebb251f2fa19cda90"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename/alias_input/tests.rs",
"bytes": 7279,
"git_blob": "579880f4f6fab0949805452451b70bdf9e43ac1e",
"sha256": "f9dd619c440a6d784792be5e41bad562ef312e07967d193b0e96cdc127debfb2"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename/editor_patch.rs",
"bytes": 11423,
"git_blob": "f30a08b8e712d3f59aefa8dc478c2db8e46512d1",
"sha256": "049c9774c1ca28cda01f9a8e024604bef282366aa52c53c17c1918dbd09c3441"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename/preview_glyph_tests.rs",
"bytes": 16247,
"git_blob": "90b3864106072a2fb39c2e1f88a877465f2dc859",
"sha256": "d45a799bd97047c6b77ff5b18334f2a71912b65d5239f2b8ce7017588ccff0f4"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 10937,
"git_blob": "4641cf7f8db309b12c7bd668dd4ea771cb456fa6",
"sha256": "06c8dc66b696f70427a190cd174be373a8114bd2f49b6e8f1152173d59e9ec29"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename/tests.rs",
"bytes": 53945,
"git_blob": "e0dfe43bcd255be1b0787e804ac6eb025c55be9e",
"sha256": "a55b5b28c6f4149c957bd9a369e67a8c10054a023b5dd2f9e5f90d441a923206"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename_layout_timing_tests.rs",
"bytes": 12253,
"git_blob": "ea37c4d44a8c54f00f9508250666b5f4a55008ce",
"sha256": "dadf8ce0ec989b9a370703a330bde4b415a813273917037fc6a732b9bf2f98ce"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_rename_timing_tests.rs",
"bytes": 13912,
"git_blob": "b7df39f71542b08ca55acc5c97d580378394f451",
"sha256": "942753946d00cab9d4a19717bec5f429af58c070e2cd1b97cb4d32ed6fb93967"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/app/source_symbols.rs",
"bytes": 18691,
"git_blob": "d992b59adf759076b057b7f242c440bacaa54c1a",
"sha256": "77cb97ec909bcaf3d952e0c6673171fd81e7ca31568248994c8c6b150e2c7f4e"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/document.rs",
"bytes": 32600,
"git_blob": "e68c312afbc17da193db832b022eabb6aeadaac6",
"sha256": "e87d7ce456bcab1dab55249021eaa8056f41b888723d143428486e31cb9ca4b1"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/document_alias_rename.rs",
"bytes": 8021,
"git_blob": "aa98e63b9c81decea42746f1766128765da47e75",
"sha256": "ce64d723a51a834699ebcf74d89d406a77b6ccc5419bcd3f63cf0e27e9fd25cb"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/document_alias_rename_tests.rs",
"bytes": 18027,
"git_blob": "5e773f46fb7ff50ebf1a43a71ee87225e8451389",
"sha256": "847dca1e50970d8a3d75c7e881754453e801f4f3d34f03a88410a334899783bc"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/guide.rs",
"bytes": 116153,
"git_blob": "d0dfce65a3c6cf418f434cfa7293e08b47a60f67",
"sha256": "9a5a0608d87e93eebd5f1c72db021f4f05391362223e0f3b2991e8b24d259f6b"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/main.rs",
"bytes": 1530,
"git_blob": "4f0505451ec37d593a5caa590efd7e7c1971d40d",
"sha256": "ab842902972443da5f55d3ea263fca72d557be2e80886368371ebec6cbc5b1ff"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/source_navigation/query.rs",
"bytes": 36149,
"git_blob": "316224b35e4def678df49de62d411076e3e979ed",
"sha256": "4f6ff7d708bb99c64b7370a1761a6099d5f97be4b319582ab4faa7bb8aa42bb9"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/source_navigation/query_tests.rs",
"bytes": 31577,
"git_blob": "0b6bd7e101dcc031647222c2c788e4be362b5510",
"sha256": "121f197dbae319f47ec59ff875733f1985c7607badb2e4c3d97ef8746d13afa9"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/source_rename.rs",
"bytes": 116,
"git_blob": "f185672c488712aa50de495561062f90c014eaaf",
"sha256": "1ead2a66d9fd7429b8cfca1338b038e7dd97e21ae76cb722f3ca4c43a520ca00"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/source_rename/origin.rs",
"bytes": 9275,
"git_blob": "3da6ccb36964f9456cbe258138838f1176bd1058",
"sha256": "11f619310fe837f20a8d495b13e557cafe258270350815c6ac8c848f51cd76ac"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/source_rename/preview.rs",
"bytes": 19903,
"git_blob": "df96bb6d43e66c9f07f94410871809b3b371e5da",
"sha256": "bf96e31986d143fce43ad1634d7028f1225aebcd9013955ca2ba18cc4fd1190d"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "crates/oa-desktop/src/views.rs",
"bytes": 68431,
"git_blob": "bedd5388cabbce972d9268ef86ca85257f34d043",
"sha256": "df415535c675f9f3aeac8a48ba53c547fe06f369ba74c585a664b898760c9d22"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "docs/language.html",
"bytes": 179620,
"git_blob": "3b7d79c0f42c70b83ebca676d1c7cb30d13b0d7a",
"sha256": "f8a9abc811047005bc616f03852b8ca2e7e784cd15eb4cd9a28cd750af62fe6b"
},
{
"side": "peer",
"pin": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"path": "docs/language.rst",
"bytes": 134869,
"git_blob": "c1ca91062bf5581a33ef7bb91ab82b4b746f0f25",
"sha256": "d2ccbb28923ea82824c5fb3a9f28f718e23bbfb695a3fb9d0ba569b8310be3c0"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/app.rs",
"bytes": 215385,
"git_blob": "11030066dd8973d3a603c1343b6b2651f9841b04",
"sha256": "4a41b062da233a72f1b04866898eeddb3f79882918a67f5f09469fe4eadd9929"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/app/source_symbols.rs",
"bytes": 18627,
"git_blob": "20d0ce55afeb7df57a95e4c1b8096b130dc89a0b",
"sha256": "99370865b548b28f32f4c6ce7e9f8eeaa0c80268a644f2ebcc4bd1e11695e59c"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/document.rs",
"bytes": 31807,
"git_blob": "8369645d832852e912bb79bef06090593fe70ede",
"sha256": "7c87280b5b8d112c34865253f8ba0f80d43bc9897771943b15dc7b5608eb17a4"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/guide.rs",
"bytes": 114509,
"git_blob": "3cb4cbce0669440ce038f8c1488ecd1c81b85a10",
"sha256": "9a6e14ea72c29f4b826e007a4a9836c1c526bd574fc14dd3476ca00a1d80b1c9"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/main.rs",
"bytes": 1511,
"git_blob": "43ab49ecb98e0a8f402af9fa85d8573483463b25",
"sha256": "cb0a919fd0df0924591283da5b55d64d525d8461640441509350be840235e51a"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/source_navigation/query.rs",
"bytes": 35200,
"git_blob": "3044fff36e5392fd9cf9b016c62c63d4b34a5769",
"sha256": "aa8e8e9d446b57c30520c4b3ca1ae9fe524cecb2f4f4f4b229d930ea0377ecca"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/source_navigation/query_tests.rs",
"bytes": 29720,
"git_blob": "e0c9cde6216202ca4bf6e31319bdb664c928d2c3",
"sha256": "a4ecc95a49d0a9325cbf1548cf95a8d367f758f8eb1f44346a2b71ef046f6668"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "crates/oa-desktop/src/views.rs",
"bytes": 67434,
"git_blob": "e91892e3216150f98539cc71c582720307689c85",
"sha256": "e5ed7640fbd2653983c418b88fd2e3612b91f8f02fa2f50f1b5dfd31aab773b2"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "docs/language.html",
"bytes": 176533,
"git_blob": "a9d889f56550eb1152310a17810c69ecbcdb55a2",
"sha256": "9660320375b5cb593169433d9b059c2be46be032a0e44fa5220b436beb3f622b"
},
{
"side": "base",
"pin": "be7a8cd8d7d63aa18e0196bbd36f9e646764c761",
"path": "docs/language.rst",
"bytes": 131984,
"git_blob": "9ea3a7f6572d46483cde621179b912ee306071ea",
"sha256": "894a1a77f97d1aa94c11762fe4e84b4d95e181a77c630324c16c307637ed27e1"
}
]
@@ -0,0 +1,195 @@
{
"base": "4db1ef543ced916179194272bf0e65dce73cb107",
"peer": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"files": [
{
"path": "crates/oa-desktop/src/app.rs",
"bytes": 233137,
"sha256": "c4af6a3bce725ed52e1e5919a2177a12c603d9beb6f0f3485925269171bef7ac",
"git_blob": "8b1edac34f9b8b96df76d4fd759bd6aaae5c0a5e",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/app/diagram_reveal.rs",
"bytes": 5996,
"sha256": "6d94b32ae64aaebdf81b80d5b8f69a5077425d51727c736a57ffb5f334b3b76a",
"git_blob": "ab4dd765b1b03556e9e7f5abf4ca4b5ae8692c1d",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/app/diagram_usages.rs",
"bytes": 18131,
"sha256": "ae7ae6f24a845549d97715d0229861bd49dc0f82d4ce0632f1c134a73de605ee",
"git_blob": "fb17862f667b597f76d77c7c320f1b0b7935758c",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45315,
"sha256": "7b13c39d4c1509d13aef2de4580b9e418cf7ac1ae87d42bd21dcd34ecc70f863",
"git_blob": "eee376e2461477a4ac123c849fb165d9e2867b2b",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/app/source_rename/alias_input.rs",
"bytes": 6940,
"sha256": "5888b50e4d6fabd8cd0c6d70986a791d8e95f190453ea37ebb251f2fa19cda90",
"git_blob": "f7240271ea87a957a686249bf629bc0c36e924fe",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_rename/alias_input/tests.rs",
"bytes": 7279,
"sha256": "f9dd619c440a6d784792be5e41bad562ef312e07967d193b0e96cdc127debfb2",
"git_blob": "579880f4f6fab0949805452451b70bdf9e43ac1e",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_rename/editor_patch.rs",
"bytes": 11423,
"sha256": "049c9774c1ca28cda01f9a8e024604bef282366aa52c53c17c1918dbd09c3441",
"git_blob": "f30a08b8e712d3f59aefa8dc478c2db8e46512d1",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_rename/navigation_union_tests.rs",
"bytes": 16422,
"sha256": "27e4529125974c832cf35e699c69bb1855f94f3bc6cd5455fde94408906c2579",
"git_blob": "dba110c2244a0921fde002af67c20d2050faf999",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_glyph_tests.rs",
"bytes": 16247,
"sha256": "d45a799bd97047c6b77ff5b18334f2a71912b65d5239f2b8ce7017588ccff0f4",
"git_blob": "90b3864106072a2fb39c2e1f88a877465f2dc859",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 10937,
"sha256": "06c8dc66b696f70427a190cd174be373a8114bd2f49b6e8f1152173d59e9ec29",
"git_blob": "4641cf7f8db309b12c7bd668dd4ea771cb456fa6",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_rename/tests.rs",
"bytes": 54012,
"sha256": "90f69af589fd1698f20ab67299785f8b6c60a99c092e615b8ddb446feff8d5f0",
"git_blob": "2b90ec6e35c0d0a10fa05268a447a0bb53131bf0",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/app/source_rename_layout_timing_tests.rs",
"bytes": 12253,
"sha256": "dadf8ce0ec989b9a370703a330bde4b415a813273917037fc6a732b9bf2f98ce",
"git_blob": "ea37c4d44a8c54f00f9508250666b5f4a55008ce",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_rename_timing_tests.rs",
"bytes": 13912,
"sha256": "942753946d00cab9d4a19717bec5f429af58c070e2cd1b97cb4d32ed6fb93967",
"git_blob": "b7df39f71542b08ca55acc5c97d580378394f451",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/app/source_symbols.rs",
"bytes": 21084,
"sha256": "43382383bc09c496a4262e7b16a01855e533c6203d046076c16032719bf7ae14",
"git_blob": "03aaade7cc7e660ab3ded9ab9920522b08f550a0",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/document.rs",
"bytes": 32600,
"sha256": "e87d7ce456bcab1dab55249021eaa8056f41b888723d143428486e31cb9ca4b1",
"git_blob": "e68c312afbc17da193db832b022eabb6aeadaac6",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/document_alias_rename.rs",
"bytes": 8021,
"sha256": "ce64d723a51a834699ebcf74d89d406a77b6ccc5419bcd3f63cf0e27e9fd25cb",
"git_blob": "aa98e63b9c81decea42746f1766128765da47e75",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/document_alias_rename_tests.rs",
"bytes": 18027,
"sha256": "847dca1e50970d8a3d75c7e881754453e801f4f3d34f03a88410a334899783bc",
"git_blob": "5e773f46fb7ff50ebf1a43a71ee87225e8451389",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/guide.rs",
"bytes": 120562,
"sha256": "8795295d86b51c5e6b589017445886132f6349c5aef0ac3542f202c1c6afb157",
"git_blob": "50a6c6c9ac64661890062f9f88343d8d52e0cef3",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/main.rs",
"bytes": 1549,
"sha256": "2230e12ddbfda7e52fa36b04bf676ca822c7807f1d31715cd0527653679c889b",
"git_blob": "cb86659ddc6a9e18026d2fc3de5f67e77a575167",
"equals_peer_raw": false
},
{
"path": "crates/oa-desktop/src/source_navigation/query.rs",
"bytes": 36149,
"sha256": "4f6ff7d708bb99c64b7370a1761a6099d5f97be4b319582ab4faa7bb8aa42bb9",
"git_blob": "316224b35e4def678df49de62d411076e3e979ed",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/source_navigation/query_tests.rs",
"bytes": 31577,
"sha256": "121f197dbae319f47ec59ff875733f1985c7607badb2e4c3d97ef8746d13afa9",
"git_blob": "0b6bd7e101dcc031647222c2c788e4be362b5510",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/source_rename.rs",
"bytes": 116,
"sha256": "1ead2a66d9fd7429b8cfca1338b038e7dd97e21ae76cb722f3ca4c43a520ca00",
"git_blob": "f185672c488712aa50de495561062f90c014eaaf",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/source_rename/origin.rs",
"bytes": 9275,
"sha256": "11f619310fe837f20a8d495b13e557cafe258270350815c6ac8c848f51cd76ac",
"git_blob": "3da6ccb36964f9456cbe258138838f1176bd1058",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/source_rename/preview.rs",
"bytes": 19903,
"sha256": "bf96e31986d143fce43ad1634d7028f1225aebcd9013955ca2ba18cc4fd1190d",
"git_blob": "df96bb6d43e66c9f07f94410871809b3b371e5da",
"equals_peer_raw": true
},
{
"path": "crates/oa-desktop/src/views.rs",
"bytes": 71224,
"sha256": "a60dea8285f844ea386e3ff429fd47e559e2a288dc88d4d98eb0f95d3684f2b1",
"git_blob": "0ebc252ab42ce1281482b7d61c4f797755f20d7d",
"equals_peer_raw": false
},
{
"path": "docs/language.html",
"bytes": 184689,
"sha256": "b0eb7199e481d8ecc8dd04bf1e1951569b6971acb0f1d1263afb40a0ec6ba14f",
"git_blob": "fb55b2ff958a785c59a671389e5a852d8a572015",
"equals_peer_raw": false
},
{
"path": "docs/language.rst",
"bytes": 139785,
"sha256": "9009c564f20f833f7d545bd286ee2b5a18fa86fbf8c6a6e68d2ffdd287a27f0f",
"git_blob": "0116a562098e9d8a5fc885442fa57621ff63e01d",
"equals_peer_raw": false
}
]
}
@@ -0,0 +1,14 @@
# Combined native acceptance — prepared, not executed
Use a fresh private copy of [examples/source-project](../../../examples/source-project/project.arcadia), the identified union executable and an isolated profile. Record the executable hash and initial hashes of all six `.arcadia` files. Existing peer native evidence does not cover this combined source.
Target the second `@assess_report` reference in `views.arcadia` (line 61 in the prepared fixture), and rename it to `assess_report_union`. The independent accepted-source oracle is six alias-token substitutions across two files: four in `sa/reporting.arcadia` and two in `views.arcadia`. Names, UUIDs, geometry, comments and the other four files must remain identical. Confirmation alone must leave all disk files unchanged.
1. **Preview visibility and scroll.** Focus the target, invoke F2, enter the replacement and Preview. On a page with actual overflow, scroll its body, switch Full details → Preview pane normally and rapidly, then scroll deliberately again and allow redraw/idle. Approval and selected file/hunk/row/segment remain unchanged; later scrolling stays put. The alias input and footer remain reachable. Cancel changes neither accepted source nor disk. Do not require the old scroll offset to survive an actually mounted Details replacement. The deterministic held-operation race is checked by the retained-widget test; native toggling alone cannot establish its timing.
2. **One modal input owner.** With Rename open, try F12 and the underlying Show in diagrams control where reachable, then Escape once. Open Show in diagrams and try F2 and the underlying Rename control while the chooser is ready, and while loading if that state is observable. Only the current modal owns input; no second dialog, hidden navigation or source edit occurs. Escape dismisses one owner, and a fresh Rename works afterward. A canceled lookup must not reopen its chooser when it completes.
3. **Confirm and immediate edit.** Rename from the same second reference. Confirm must focus the editor and select exactly that renamed alias. Without clicking, type `assess_report_probe`: only the selected occurrence becomes an unapplied text draft. Ctrl+Z restores `assess_report_union` without undoing the accepted rename. Disk remains baseline.
4. **Diagram usage and model history.** From the restored renamed reference, Show in diagrams and visit each destination, returning to Text between visits. Expected destinations are **SA — system boundary and functions**, placement `sa_assessment_view` (`00000000-0000-4000-8000-000000000714`), and **SA — functional containment**, placement `assessment_view` (`00000000-0000-4000-8000-000000000723`). Each reveals the visible **Assess report** representation with a readable marker after layout settles. Navigation adds no history entry. One Undo model restores all six old tokens; one Redo model restores all six new tokens. Before Save disk stays baseline; after Save and native reopen, exactly the two expected files contain exactly the six substitutions.
If the example has no genuinely scrollable preview at the chosen window size, materialize the existing `paged_fixture()` in [preview_scroll_tests.rs](../../../crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs) as a separate native fixture. Preserve its exact generation inputs; a non-overflowing preview cannot establish scroll acceptance.
Record actual observations, source/disk/history oracles, logs, window size and clean closure against the matching executable. Compilation and screenshots alone do not establish these outcomes. Linux remains user-paused.
@@ -0,0 +1,168 @@
{
"base": "4db1ef543ced916179194272bf0e65dce73cb107",
"peer": "cbc49a7802fb1bba0dd284fd904c700a0703ae70",
"files": [
{
"path": "crates/oa-desktop/src/app.rs",
"bytes": 233137,
"sha256": "c4af6a3bce725ed52e1e5919a2177a12c603d9beb6f0f3485925269171bef7ac",
"git_blob": "8b1edac34f9b8b96df76d4fd759bd6aaae5c0a5e"
},
{
"path": "crates/oa-desktop/src/app/diagram_reveal.rs",
"bytes": 5996,
"sha256": "6d94b32ae64aaebdf81b80d5b8f69a5077425d51727c736a57ffb5f334b3b76a",
"git_blob": "ab4dd765b1b03556e9e7f5abf4ca4b5ae8692c1d"
},
{
"path": "crates/oa-desktop/src/app/diagram_usages.rs",
"bytes": 18131,
"sha256": "ae7ae6f24a845549d97715d0229861bd49dc0f82d4ce0632f1c134a73de605ee",
"git_blob": "fb17862f667b597f76d77c7c320f1b0b7935758c"
},
{
"path": "crates/oa-desktop/src/app/source_rename.rs",
"bytes": 45705,
"sha256": "4714cde897a7a365958a6014f6a546df682a963a48c2fd9d918ddb3c5500268d",
"git_blob": "a4e41d1a3ccd8c2a37e763c3ab50ff6cac8935af"
},
{
"path": "crates/oa-desktop/src/app/source_rename/alias_input.rs",
"bytes": 6940,
"sha256": "5888b50e4d6fabd8cd0c6d70986a791d8e95f190453ea37ebb251f2fa19cda90",
"git_blob": "f7240271ea87a957a686249bf629bc0c36e924fe"
},
{
"path": "crates/oa-desktop/src/app/source_rename/alias_input/tests.rs",
"bytes": 7279,
"sha256": "f9dd619c440a6d784792be5e41bad562ef312e07967d193b0e96cdc127debfb2",
"git_blob": "579880f4f6fab0949805452451b70bdf9e43ac1e"
},
{
"path": "crates/oa-desktop/src/app/source_rename/editor_patch.rs",
"bytes": 11423,
"sha256": "049c9774c1ca28cda01f9a8e024604bef282366aa52c53c17c1918dbd09c3441",
"git_blob": "f30a08b8e712d3f59aefa8dc478c2db8e46512d1"
},
{
"path": "crates/oa-desktop/src/app/source_rename/navigation_union_tests.rs",
"bytes": 16422,
"sha256": "27e4529125974c832cf35e699c69bb1855f94f3bc6cd5455fde94408906c2579",
"git_blob": "dba110c2244a0921fde002af67c20d2050faf999"
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_glyph_tests.rs",
"bytes": 16247,
"sha256": "d45a799bd97047c6b77ff5b18334f2a71912b65d5239f2b8ce7017588ccff0f4",
"git_blob": "90b3864106072a2fb39c2e1f88a877465f2dc859"
},
{
"path": "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs",
"bytes": 13821,
"sha256": "ca8d49b5de9e4b848d73cc4c0a4884c0a006a48c3e171f32fb496eac4d6aa180",
"git_blob": "3025e756299bab0c0c48aa236e99dd9788e557ee"
},
{
"path": "crates/oa-desktop/src/app/source_rename/tests.rs",
"bytes": 54012,
"sha256": "90f69af589fd1698f20ab67299785f8b6c60a99c092e615b8ddb446feff8d5f0",
"git_blob": "2b90ec6e35c0d0a10fa05268a447a0bb53131bf0"
},
{
"path": "crates/oa-desktop/src/app/source_rename_layout_timing_tests.rs",
"bytes": 12253,
"sha256": "dadf8ce0ec989b9a370703a330bde4b415a813273917037fc6a732b9bf2f98ce",
"git_blob": "ea37c4d44a8c54f00f9508250666b5f4a55008ce"
},
{
"path": "crates/oa-desktop/src/app/source_rename_timing_tests.rs",
"bytes": 13912,
"sha256": "942753946d00cab9d4a19717bec5f429af58c070e2cd1b97cb4d32ed6fb93967",
"git_blob": "b7df39f71542b08ca55acc5c97d580378394f451"
},
{
"path": "crates/oa-desktop/src/app/source_symbols.rs",
"bytes": 21084,
"sha256": "43382383bc09c496a4262e7b16a01855e533c6203d046076c16032719bf7ae14",
"git_blob": "03aaade7cc7e660ab3ded9ab9920522b08f550a0"
},
{
"path": "crates/oa-desktop/src/document.rs",
"bytes": 32600,
"sha256": "e87d7ce456bcab1dab55249021eaa8056f41b888723d143428486e31cb9ca4b1",
"git_blob": "e68c312afbc17da193db832b022eabb6aeadaac6"
},
{
"path": "crates/oa-desktop/src/document_alias_rename.rs",
"bytes": 8021,
"sha256": "ce64d723a51a834699ebcf74d89d406a77b6ccc5419bcd3f63cf0e27e9fd25cb",
"git_blob": "aa98e63b9c81decea42746f1766128765da47e75"
},
{
"path": "crates/oa-desktop/src/document_alias_rename_tests.rs",
"bytes": 18027,
"sha256": "847dca1e50970d8a3d75c7e881754453e801f4f3d34f03a88410a334899783bc",
"git_blob": "5e773f46fb7ff50ebf1a43a71ee87225e8451389"
},
{
"path": "crates/oa-desktop/src/guide.rs",
"bytes": 120562,
"sha256": "8795295d86b51c5e6b589017445886132f6349c5aef0ac3542f202c1c6afb157",
"git_blob": "50a6c6c9ac64661890062f9f88343d8d52e0cef3"
},
{
"path": "crates/oa-desktop/src/main.rs",
"bytes": 1549,
"sha256": "2230e12ddbfda7e52fa36b04bf676ca822c7807f1d31715cd0527653679c889b",
"git_blob": "cb86659ddc6a9e18026d2fc3de5f67e77a575167"
},
{
"path": "crates/oa-desktop/src/source_navigation/query.rs",
"bytes": 36149,
"sha256": "4f6ff7d708bb99c64b7370a1761a6099d5f97be4b319582ab4faa7bb8aa42bb9",
"git_blob": "316224b35e4def678df49de62d411076e3e979ed"
},
{
"path": "crates/oa-desktop/src/source_navigation/query_tests.rs",
"bytes": 31577,
"sha256": "121f197dbae319f47ec59ff875733f1985c7607badb2e4c3d97ef8746d13afa9",
"git_blob": "0b6bd7e101dcc031647222c2c788e4be362b5510"
},
{
"path": "crates/oa-desktop/src/source_rename.rs",
"bytes": 116,
"sha256": "1ead2a66d9fd7429b8cfca1338b038e7dd97e21ae76cb722f3ca4c43a520ca00",
"git_blob": "f185672c488712aa50de495561062f90c014eaaf"
},
{
"path": "crates/oa-desktop/src/source_rename/origin.rs",
"bytes": 9275,
"sha256": "11f619310fe837f20a8d495b13e557cafe258270350815c6ac8c848f51cd76ac",
"git_blob": "3da6ccb36964f9456cbe258138838f1176bd1058"
},
{
"path": "crates/oa-desktop/src/source_rename/preview.rs",
"bytes": 19903,
"sha256": "bf96e31986d143fce43ad1634d7028f1225aebcd9013955ca2ba18cc4fd1190d",
"git_blob": "df96bb6d43e66c9f07f94410871809b3b371e5da"
},
{
"path": "crates/oa-desktop/src/views.rs",
"bytes": 71224,
"sha256": "a60dea8285f844ea386e3ff429fd47e559e2a288dc88d4d98eb0f95d3684f2b1",
"git_blob": "0ebc252ab42ce1281482b7d61c4f797755f20d7d"
},
{
"path": "docs/language.html",
"bytes": 184689,
"sha256": "b0eb7199e481d8ecc8dd04bf1e1951569b6971acb0f1d1263afb40a0ec6ba14f",
"git_blob": "fb55b2ff958a785c59a671389e5a852d8a572015"
},
{
"path": "docs/language.rst",
"bytes": 139785,
"sha256": "9009c564f20f833f7d545bd286ee2b5a18fa86fbf8c6a6e68d2ffdd287a27f0f",
"git_blob": "0116a562098e9d8a5fc885442fa57621ff63e01d"
}
]
}
@@ -0,0 +1,3 @@
*.log -text -whitespace
*.json -text whitespace=cr-at-eol
*.ps1 -text whitespace=cr-at-eol
@@ -0,0 +1,27 @@
# Measured label and corridor evaluator: Windows results
Tested source: 903d85dda403b93731c05a08b97fa7d257d012dd. The portable standalone.rs in ../scenario-label-measurement-20260913 imports the actual label, label-test and corridor source files; it does not wire the desktop renderer. All five captured source/lock/wrapper inputs remained byte-for-byte unchanged. Raw bindings are in artifacts.json; binary.json identifies the executed test artifact.
## Actual result
The isolated Rust test executable compiled with warnings denied, then listed exactly18 tests. Normal execution passed17 with0 failures and1 intentionally ignored global-font mutation (2.72 seconds). A separate fresh process selected and passed that exact ignored test:1 passed,0 failed,0 ignored,17 filtered (0.57 seconds). In total all18 selected evaluators passed. No executable or source changed between runs.
The real-font test independently admitted known embedded Iced-Icons font bytes, then loaded owned valid bytes into the actual global FontSystem. Face count changed167→168 and Version changed0→1. Every retained-label consumer and old-session resolve rejected the stale font epoch without remeasurement. This tests version/lifetime rejection, not a different chosen font's visual metrics, dynamic WGPU cache recovery or concurrent font loading.
The other tests cover complete realistic message names and distinguishing suffixes at sufficient width, Unicode grapheme/source-range mapping, paragraph metric agreement at three zooms, narrow/empty labels, resident cache/session invalidation, finite scaled/world positions and dimensions, and six connected-corridor cases. Corridor evaluation takes explicit final geometry; passing synthetic self/reversed bands does not mean production self/reversed placement has been implemented.
## Compiler and reproduction boundary
compile-receipt.json records the exact rustc1.98.1 invocation, x86_64-pc-windows-gnullvm host, selected pinned Iced0.14/unicode rlibs, native import-library search path, start/end and exit0. compile.ps1 contains the invoked command. The successful compiler emitted no output, so no successful compile.log was created. available-dependencies.json binds both locally available Iced variants (only iced-ee73abd722c72024 was selected), the used Unicode dependency and the locked Windows native import library. This is a direct rustc evaluator using existing dependency artifacts, not a Cargo workspace gate or a reproducible-from-source rebuild of those third-party dependencies.
To reproduce on a prepared matching Windows checkout, build its locked dependencies, resolve the matching Iced/unicode rlibs and windows_x86_64_gnullvm0.52.6 native lib directory, then use the recorded rustc --test invocation against the versioned standalone.rs, substituting those local paths. List the resulting executable and require exactly18 tests before running it. Run ordinary tests normally, then use ../scenario-label-measurement-20260913/run-font-version-test.ps1 with the exact executable SHA256 and standalone test name in a fresh evidence directory. Supply the matching compiler runtime DLL environment. The real-font test must run alone in its own process.
## Preserved failed attempts
Attempt1 used source9b56a337f8db2c3f63e33e06468cb8de2b93fbf1 and failed module resolution under the standalone path import. Source903d85d adds an explicit test-module path; this is the only source correction after9b. Attempt2 reached the linker on903d85d but lacked the Windows native import-library search path. Attempt3 added that actual locked gnullvm library path and succeeded without another source change. Both failures' raw commands, receipts, inputs and logs are retained; they are not successful tests. Original unformatted/review attempts are preserved in the linked preparation directory.
## Remaining integration work
The source remains unwired. Choose structural scene/corridor bounds first, then camera zoom, then resolve labels at that zoom and settled-font session. Paint, backplate, hit and operation-field navigation must consume the same result. Navigation needs an explicit destination zoom preserved through lookup, Choose and deferred centering; its current independent scale1 lookup cannot establish destination visibility. Subtract padded activation/card/header blockers over the entire label band before choosing one connected interval. Do not repeatedly feed measured label bounds back into Fit, or rely on paint order to prevent foreground text crossing a bar.
Actual Scene/lifecycle alignment, destination-camera consistency, readable self/reversed arrows, glyph ink/overhang, WGPU painting, native hit/navigation workflows, persistence and drag behavior remain unverified. This successful isolated evaluator is not #20 or full OA/SA acceptance. Linux remains user-deferred. Live coordination is in Gitea issues20,7 and27.
@@ -0,0 +1,128 @@
[
{
"path": "attempt1/compile.log",
"bytes": 974,
"sha256": "21a2407b76ee10009c3e16fe9e3277e0943d5442560c57538ee0e8b351fe4a53",
"gitBlob": "8e3361b7a4839e0a5548437e983143818a3342a5"
},
{
"path": "attempt1/compile-receipt.json",
"bytes": 1286,
"sha256": "72ad2111043ba6a56a06d9f5b65c0cc54cce38bc403ad43e4068098ad467fb5d",
"gitBlob": "6e21a36d4db62d8577955fd5527ad19828fbb219"
},
{
"path": "attempt1/compile.ps1",
"bytes": 1503,
"sha256": "6d71ae98792110570a7b8873381d1e855c763a88d96d6862c83217fca54ee106",
"gitBlob": "39f5bc4d50d9e2f43630902a7ac7d7cb69c006e4"
},
{
"path": "attempt1/inputs-before.json",
"bytes": 1092,
"sha256": "3b5132b9b436de358330de8cbbfbabeb3e7ed0eba809e743d8a0847d7d256164",
"gitBlob": "c21d99496226076c44b7a169346ff7eba3a2f72b"
},
{
"path": "attempt2/compile.log",
"bytes": 6136,
"sha256": "35d23a128c26f732d9ce95f277e8ffc0b89c5ff58aa8284f31f2124dd698ae31",
"gitBlob": "3638e1efb81c962d5296b1aaa4074ca3bb6e67bf"
},
{
"path": "attempt2/compile-receipt.json",
"bytes": 1295,
"sha256": "8de38e858df371f91b031e97251d89c58d16c448bff53735073dbf85a99a9b40",
"gitBlob": "c8a3aa1c87e4e870492ebfd4fc3cde514899ff51"
},
{
"path": "attempt2/compile.ps1",
"bytes": 1503,
"sha256": "d30fd870182f18f85333b6d0e82c7014d6853d640affd5227155efbee736c75f",
"gitBlob": "02a286298ea09560968a626ef31e6ac9df81efaa"
},
{
"path": "attempt2/inputs-before.json",
"bytes": 1092,
"sha256": "a1a02602713813cdc09b1c865a1d32c019c27f1e10c4cafdfc9aac827cb79f51",
"gitBlob": "4afbdce92fca7147eb3cc8a2e7507fd379a75948"
},
{
"path": "available-dependencies.json",
"bytes": 781,
"sha256": "850d702e21094f189d827bc4ca825b80ef46ba1a29a189c6d00ed3a9a90f56fc",
"gitBlob": "5b74307f371ec165827339dde6a20859e09beca8"
},
{
"path": "binary.json",
"bytes": 133,
"sha256": "a7a655168a84c3fb01482c79779c3eb786caea4dd678a9def74ba72d06d25ba9",
"gitBlob": "437ff4fb0ba968e51c7288fcd26c8dcf154fdfb4"
},
{
"path": "compile-receipt.json",
"bytes": 1467,
"sha256": "bc9f8aa6760520f78ba4305e10e4561a01100b858824f3700e74976379201009",
"gitBlob": "57fad30abf1f7807f8860897f830ce2d9cbbf32d"
},
{
"path": "compile.ps1",
"bytes": 1665,
"sha256": "3bc1e312351350fef1510b53d460cf37943d952310482cff30cce7ab536fc626",
"gitBlob": "b28998982084b35043c0ccf0960fd20ace7d096b"
},
{
"path": "inputs-before.json",
"bytes": 1092,
"sha256": "a1a02602713813cdc09b1c865a1d32c019c27f1e10c4cafdfc9aac827cb79f51",
"gitBlob": "4afbdce92fca7147eb3cc8a2e7507fd379a75948"
},
{
"path": "inputs-after.json",
"bytes": 1113,
"sha256": "b078a8d98665fac4bdd57856a450d4dd8e66261568b27b8d57ffcbf831204eb2",
"gitBlob": "838608e11b0a4db54b98c62c38c5d85c3b38181e"
},
{
"path": "list.log",
"bytes": 1770,
"sha256": "acd0f1eadde1187e6949aa4dbb57732cc9b872f0d586223e9c6e179a63d05a77",
"gitBlob": "df9986911a54d13754fb8171933f121a91ae26ca"
},
{
"path": "run-tests.ps1",
"bytes": 2006,
"sha256": "56923306bb50d88cef5d67638645d6a13c2ce5e09efbb3b17b28f157feddbd3a",
"gitBlob": "b6eb23cf3de8baeeb16c6e254213ae74c7788381"
},
{
"path": "tests-receipt.json",
"bytes": 430,
"sha256": "ae4d0104cc2dc5a06f1cffe9f46a6c6d00ab68a5c27dbd92d460c81ad0f98fde",
"gitBlob": "0f9a924d8184697e37fc15a0c684f991cdbf7952"
},
{
"path": "tests.log",
"bytes": 2056,
"sha256": "5865a550531a989302ad4453c66c462672f293e47f8c88b37eef7764d567c38b",
"gitBlob": "759df3ccb33855be97acf7121d0448c408783c0d"
},
{
"path": "font-version/selection.log",
"bytes": 112,
"sha256": "06c79ed30f9df147d325ca7b8da78cfd949e7f595d1ec18e158ca59d2f92a0c1",
"gitBlob": "dc985bd84a4e860ae2a717099ba05b3020151c0f"
},
{
"path": "font-version/test.log",
"bytes": 320,
"sha256": "27a2d0e71a732900d265d0224973c49a6b6d41481743cc2cd795dc14f6e4097d",
"gitBlob": "4606db46f2370c94f48c9e99d25c263466374147"
},
{
"path": "font-version/receipt.json",
"bytes": 601,
"sha256": "43001d79dcb326fef76c4f84d9a8c00772c5b700860d316be9c91a99d88b94f1",
"gitBlob": "f8b2676ca8ff91624f1c4378ff2bfaf2c4dcc7ec"
}
]
@@ -0,0 +1,29 @@
{
"source": "9b56a337f8db2c3f63e33e06468cb8de2b93fbf1",
"command": "rustc",
"arguments": [
"--edition=2024",
"--test",
"--crate-name",
"scenario_label_probe",
"-C",
"debuginfo=0",
"-C",
"linker=clang.exe",
"-D",
"warnings",
"--extern",
"iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libiced-ee73abd722c72024.rlib",
"--extern",
"unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libunicode_segmentation-85698043f7780858.rlib",
"-L",
"dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps",
"-o",
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\scenario-label-gates-20260913\\label_tests.exe",
"C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/standalone.rs"
],
"compiler": "rustc 1.98.1 (48a229cea 2026-09-01)\r\nbinary: rustc\r\ncommit-hash: 48a229ceaefd4985c50990b14116b6d856af0985\r\ncommit-date: 2026-09-01\r\nhost: x86_64-pc-windows-gnullvm\r\nrelease: 1.98.1\r\nLLVM version: 22.1.8\r\n",
"startUtc": "2026-09-13T01:36:30.2307437Z",
"endUtc": "2026-09-13T01:36:33.5180878Z",
"exitCode": 1
}
@@ -0,0 +1,12 @@
error[E0583]: file not found for module `tests`
--> C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913\..\..\..\crates\oa-desktop\src\behavior\scenario_label.rs:429:1
|
429 | mod tests;
| ^^^^^^^^^^
|
= help: to create the module `tests`, create file "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913\..\..\..\crates\oa-desktop\src\behavior\tests.rs" or "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913\..\..\..\crates\oa-desktop\src\behavior\tests\mod.rs"
= note: if there is a `mod tests` elsewhere in the crate already, import it with `use crate::...` instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0583`.
@@ -0,0 +1,12 @@
$ErrorActionPreference = 'Stop'
. 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/env.ps1'
$taskArgs = @('--edition=2024', '--test', '--crate-name', 'scenario_label_probe', '-C', 'debuginfo=0', '-C', 'linker=clang.exe', '-D', 'warnings', '--extern', 'iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libiced-ee73abd722c72024.rlib', '--extern', 'unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libunicode_segmentation-85698043f7780858.rlib', '-L', 'dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps', '-o', (Join-Path $PSScriptRoot 'label_tests.exe'), 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/standalone.rs')
$taskLog=Join-Path $PSScriptRoot 'compile.log'
$taskReceipt=Join-Path $PSScriptRoot 'compile-receipt.json'
if ((Test-Path -LiteralPath $taskLog) -or (Test-Path -LiteralPath $taskReceipt)) { throw 'Refusing to replace compile evidence' }
$taskStart=[DateTime]::UtcNow.ToString('o')
$taskVersion=(& rustc --version --verbose | Out-String)
& rustc @taskArgs 2>&1 | Tee-Object -FilePath $taskLog
$taskExit=$LASTEXITCODE
[ordered]@{source='9b56a337f8db2c3f63e33e06468cb8de2b93fbf1';command='rustc';arguments=$taskArgs;compiler=$taskVersion;startUtc=$taskStart;endUtc=[DateTime]::UtcNow.ToString('o');exitCode=$taskExit} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $taskReceipt -Encoding utf8
exit $taskExit
@@ -0,0 +1,31 @@
{
"source": "9b56a337f8db2c3f63e33e06468cb8de2b93fbf1",
"workspace": "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913",
"files": [
{
"path": "Cargo.lock",
"bytes": 119478,
"sha256": "78b405543fb7b66287457e9476ca1e555838fcdadade114aeab9b76597726787"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label.rs",
"bytes": 15102,
"sha256": "0c1e5b934a5ea10a862be8bb7e035da7c1463f32abd3dc2e5c029dac4801a8d1"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/tests.rs",
"bytes": 17960,
"sha256": "cdd5510ff45645a9f82272d8868d1862111ebcd054577242e06103443658165b"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/corridor.rs",
"bytes": 7056,
"sha256": "d369805099b7f6182b77ad8467f843a31d21502700bd6ce50b548dd22a022fe5"
},
{
"path": "harness/experiments/scenario-label-measurement-20260913/standalone.rs",
"bytes": 263,
"sha256": "8a52f01004e6749308a55c9f183fbaf66ef2d4ece2bd7a1bb97d39abf5fee797"
}
]
}
@@ -0,0 +1,29 @@
{
"source": "903d85dda403b93731c05a08b97fa7d257d012dd",
"command": "rustc",
"arguments": [
"--edition=2024",
"--test",
"--crate-name",
"scenario_label_probe",
"-C",
"debuginfo=0",
"-C",
"linker=clang.exe",
"-D",
"warnings",
"--extern",
"iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libiced-ee73abd722c72024.rlib",
"--extern",
"unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libunicode_segmentation-85698043f7780858.rlib",
"-L",
"dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps",
"-o",
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\scenario-label-gates-attempt2-20260913\\label_tests.exe",
"C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/standalone.rs"
],
"compiler": "rustc 1.98.1 (48a229cea 2026-09-01)\r\nbinary: rustc\r\ncommit-hash: 48a229ceaefd4985c50990b14116b6d856af0985\r\ncommit-date: 2026-09-01\r\nhost: x86_64-pc-windows-gnullvm\r\nrelease: 1.98.1\r\nLLVM version: 22.1.8\r\n",
"startUtc": "2026-09-13T01:37:15.9984031Z",
"endUtc": "2026-09-13T01:37:24.2982227Z",
"exitCode": 1
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
$ErrorActionPreference = 'Stop'
. 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/env.ps1'
$taskArgs = @('--edition=2024', '--test', '--crate-name', 'scenario_label_probe', '-C', 'debuginfo=0', '-C', 'linker=clang.exe', '-D', 'warnings', '--extern', 'iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libiced-ee73abd722c72024.rlib', '--extern', 'unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libunicode_segmentation-85698043f7780858.rlib', '-L', 'dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps', '-o', (Join-Path $PSScriptRoot 'label_tests.exe'), 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/standalone.rs')
$taskLog=Join-Path $PSScriptRoot 'compile.log'
$taskReceipt=Join-Path $PSScriptRoot 'compile-receipt.json'
if ((Test-Path -LiteralPath $taskLog) -or (Test-Path -LiteralPath $taskReceipt)) { throw 'Refusing to replace compile evidence' }
$taskStart=[DateTime]::UtcNow.ToString('o')
$taskVersion=(& rustc --version --verbose | Out-String)
& rustc @taskArgs 2>&1 | Tee-Object -FilePath $taskLog
$taskExit=$LASTEXITCODE
[ordered]@{source='903d85dda403b93731c05a08b97fa7d257d012dd';command='rustc';arguments=$taskArgs;compiler=$taskVersion;startUtc=$taskStart;endUtc=[DateTime]::UtcNow.ToString('o');exitCode=$taskExit} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $taskReceipt -Encoding utf8
exit $taskExit
@@ -0,0 +1,31 @@
{
"source": "903d85dda403b93731c05a08b97fa7d257d012dd",
"workspace": "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913",
"files": [
{
"path": "Cargo.lock",
"bytes": 119478,
"sha256": "78b405543fb7b66287457e9476ca1e555838fcdadade114aeab9b76597726787"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label.rs",
"bytes": 15138,
"sha256": "621f7e50372d0162f0643219a41f5e374fc9cf28a3a42aee6b8728e5271ec6c4"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/tests.rs",
"bytes": 17960,
"sha256": "cdd5510ff45645a9f82272d8868d1862111ebcd054577242e06103443658165b"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/corridor.rs",
"bytes": 7056,
"sha256": "d369805099b7f6182b77ad8467f843a31d21502700bd6ce50b548dd22a022fe5"
},
{
"path": "harness/experiments/scenario-label-measurement-20260913/standalone.rs",
"bytes": 263,
"sha256": "8a52f01004e6749308a55c9f183fbaf66ef2d4ece2bd7a1bb97d39abf5fee797"
}
]
}
@@ -0,0 +1,22 @@
[
{
"name": "libiced-ee73abd722c72024.rlib",
"bytes": 408214,
"sha256": "d6afba083a249722c44ac10c3cab8f2f7a2665faecbebfbad9706d51df571cf7"
},
{
"name": "libiced-a4e48b220c5d9711.rlib",
"bytes": 408162,
"sha256": "19db042a126948a970b8494281243d7044df10d857b14ca89a4d876b69f1ce9c"
},
{
"name": "libunicode_segmentation-85698043f7780858.rlib",
"bytes": 1341698,
"sha256": "5b360e6eb17c58b9c1b00ed8b101ac2f2958d328789b5f45087c2352de2d41a5"
},
{
"path": "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/windows_x86_64_gnullvm-0.52.6/lib/libwindows.0.52.0.a",
"bytes": 3878066,
"sha256": "7d7617eae9e20e8026c39861f3c59906772bc8370996b9de45d12f53e4c78465"
}
]
@@ -0,0 +1,5 @@
{
"path": "label_tests.exe",
"bytes": 13569536,
"sha256": "83f33bb63f4b70cbf4c20b39556a3b8e34ef48132928c09a97cebce1a0789e4f"
}
@@ -0,0 +1,31 @@
{
"source": "903d85dda403b93731c05a08b97fa7d257d012dd",
"command": "rustc",
"arguments": [
"--edition=2024",
"--test",
"--crate-name",
"scenario_label_probe",
"-C",
"debuginfo=0",
"-C",
"linker=clang.exe",
"-D",
"warnings",
"--extern",
"iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libiced-ee73abd722c72024.rlib",
"--extern",
"unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libunicode_segmentation-85698043f7780858.rlib",
"-L",
"dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps",
"-L",
"native=C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/windows_x86_64_gnullvm-0.52.6/lib",
"-o",
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\scenario-label-gates-attempt3-20260913\\label_tests.exe",
"C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/standalone.rs"
],
"compiler": "rustc 1.98.1 (48a229cea 2026-09-01)\r\nbinary: rustc\r\ncommit-hash: 48a229ceaefd4985c50990b14116b6d856af0985\r\ncommit-date: 2026-09-01\r\nhost: x86_64-pc-windows-gnullvm\r\nrelease: 1.98.1\r\nLLVM version: 22.1.8\r\n",
"startUtc": "2026-09-13T01:38:34.6483724Z",
"endUtc": "2026-09-13T01:38:47.2474651Z",
"exitCode": 0
}
@@ -0,0 +1,12 @@
$ErrorActionPreference = 'Stop'
. 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/env.ps1'
$taskArgs = @('--edition=2024', '--test', '--crate-name', 'scenario_label_probe', '-C', 'debuginfo=0', '-C', 'linker=clang.exe', '-D', 'warnings', '--extern', 'iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libiced-ee73abd722c72024.rlib', '--extern', 'unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps/libunicode_segmentation-85698043f7780858.rlib', '-L', 'dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target/debug/deps', '-L', 'native=C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/windows_x86_64_gnullvm-0.52.6/lib', '-o', (Join-Path $PSScriptRoot 'label_tests.exe'), 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/standalone.rs')
$taskLog=Join-Path $PSScriptRoot 'compile.log'
$taskReceipt=Join-Path $PSScriptRoot 'compile-receipt.json'
if ((Test-Path -LiteralPath $taskLog) -or (Test-Path -LiteralPath $taskReceipt)) { throw 'Refusing to replace compile evidence' }
$taskStart=[DateTime]::UtcNow.ToString('o')
$taskVersion=(& rustc --version --verbose | Out-String)
& rustc @taskArgs 2>&1 | Tee-Object -FilePath $taskLog
$taskExit=$LASTEXITCODE
[ordered]@{source='903d85dda403b93731c05a08b97fa7d257d012dd';command='rustc';arguments=$taskArgs;compiler=$taskVersion;startUtc=$taskStart;endUtc=[DateTime]::UtcNow.ToString('o');exitCode=$taskExit} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $taskReceipt -Encoding utf8
exit $taskExit
@@ -0,0 +1,11 @@
{
"executable": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\scenario-label-gates-attempt3-20260913\\label_tests.exe",
"passedExactlyOne": true,
"sha256": "83F33BB63F4B70CBF4C20B39556A3B8E34EF48132928C09A97CEBCE1A0789E4F",
"exitCode": 0,
"exactSelection": true,
"startedUtc": "2026-09-13T01:39:39.8167547Z",
"scope": "Actual global font Version invalidation and artifact rejection; no WGPU dynamic cache recovery claim",
"finishedUtc": "2026-09-13T01:39:44.2165018Z",
"test": "scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session"
}
@@ -0,0 +1,4 @@
scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session: test
1 test, 0 benchmarks
@@ -0,0 +1,8 @@
running 1 test
test scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session ... Valid Iced-Icons load: faces 167->168, font Version Version(0)->Version(1); old label and session rejected
ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 17 filtered out; finished in 0.57s
@@ -0,0 +1,32 @@
{
"source": "903d85dda403b93731c05a08b97fa7d257d012dd",
"workspace": "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913",
"files": [
{
"path": "Cargo.lock",
"bytes": 119478,
"sha256": "78b405543fb7b66287457e9476ca1e555838fcdadade114aeab9b76597726787"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label.rs",
"bytes": 15138,
"sha256": "621f7e50372d0162f0643219a41f5e374fc9cf28a3a42aee6b8728e5271ec6c4"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/tests.rs",
"bytes": 17960,
"sha256": "cdd5510ff45645a9f82272d8868d1862111ebcd054577242e06103443658165b"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/corridor.rs",
"bytes": 7056,
"sha256": "d369805099b7f6182b77ad8467f843a31d21502700bd6ce50b548dd22a022fe5"
},
{
"path": "harness/experiments/scenario-label-measurement-20260913/standalone.rs",
"bytes": 263,
"sha256": "8a52f01004e6749308a55c9f183fbaf66ef2d4ece2bd7a1bb97d39abf5fee797"
}
],
"unchanged": true
}
@@ -0,0 +1,31 @@
{
"source": "903d85dda403b93731c05a08b97fa7d257d012dd",
"workspace": "C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913",
"files": [
{
"path": "Cargo.lock",
"bytes": 119478,
"sha256": "78b405543fb7b66287457e9476ca1e555838fcdadade114aeab9b76597726787"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label.rs",
"bytes": 15138,
"sha256": "621f7e50372d0162f0643219a41f5e374fc9cf28a3a42aee6b8728e5271ec6c4"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/tests.rs",
"bytes": 17960,
"sha256": "cdd5510ff45645a9f82272d8868d1862111ebcd054577242e06103443658165b"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/corridor.rs",
"bytes": 7056,
"sha256": "d369805099b7f6182b77ad8467f843a31d21502700bd6ce50b548dd22a022fe5"
},
{
"path": "harness/experiments/scenario-label-measurement-20260913/standalone.rs",
"bytes": 263,
"sha256": "8a52f01004e6749308a55c9f183fbaf66ef2d4ece2bd7a1bb97d39abf5fee797"
}
]
}
@@ -0,0 +1,20 @@
scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session: test
scenario_label::tests::adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes: test
scenario_label::tests::bounds_translation_rejects_nonfinite_world_positions_and_endpoints: test
scenario_label::tests::empty_and_too_narrow_labels_never_claim_source_visibility: test
scenario_label::tests::finite_scaled_metrics_cannot_escape_as_infinite_world_dimensions: test
scenario_label::tests::grapheme_prefixes_and_zoom_match_actual_paragraph_layout: test
scenario_label::tests::malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate: test
scenario_label::tests::normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation: test
scenario_label::tests::redraws_reuse_resolution_but_width_zoom_and_style_are_dependencies: test
scenario_label::tests::resident_entry_is_remeasured_for_a_fresh_settled_render_session: test
scenario_label::tests::resident_entry_remeasures_after_local_font_epoch_invalidation: test
scenario_label::tests::retained_artifact_rejects_other_zoom_and_render_session_for_every_consumer: test
scenario_label_corridor::tests::caller_defined_self_and_reversed_corridors_keep_their_geometry: test
scenario_label_corridor::tests::complete_padded_band_and_touching_boundaries_are_obstructed: test
scenario_label_corridor::tests::invalid_or_overflowing_geometry_never_enters_a_corridor: test
scenario_label_corridor::tests::long_message_does_not_cross_an_intermediate_execution: test
scenario_label_corridor::tests::overlapping_unsorted_blockers_merge_and_widest_gap_wins: test
scenario_label_corridor::tests::ties_keep_the_nearest_anchor_then_choose_leftmost: test
18 tests, 0 benchmarks
@@ -0,0 +1,21 @@
$ErrorActionPreference='Stop'
. 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/toolchain/env.ps1'
$taskExe=Join-Path $PSScriptRoot 'label_tests.exe'
$taskHash=(Get-FileHash -LiteralPath $taskExe -Algorithm SHA256).Hash.ToLowerInvariant()
$taskListLog=Join-Path $PSScriptRoot 'list.log'
$taskTestsLog=Join-Path $PSScriptRoot 'tests.log'
if ((Test-Path -LiteralPath $taskListLog) -or (Test-Path -LiteralPath $taskTestsLog)) { throw 'Refusing to overwrite runtime evidence' }
& $taskExe --list 2>&1 | Tee-Object -FilePath $taskListLog
$taskListExit=$LASTEXITCODE
$taskListed=Get-Content -Raw -LiteralPath $taskListLog
$taskCount=[regex]::Matches($taskListed,'(?m)^[^\r\n]+: test\r?$').Count
if ($taskListExit -ne 0 -or $taskCount -ne 18) { throw 'The standalone listing must contain exactly18 tests' }
$taskStart=[DateTime]::UtcNow.ToString('o')
& $taskExe --nocapture 2>&1 | Tee-Object -FilePath $taskTestsLog
$taskExit=$LASTEXITCODE
$taskOutput=Get-Content -Raw -LiteralPath $taskTestsLog
$taskPassed=$taskExit -eq 0 -and $taskOutput -match 'test result: ok\. 17 passed; 0 failed; 1 ignored;'
[ordered]@{source='903d85dda403b93731c05a08b97fa7d257d012dd';executable=$taskExe;sha256=$taskHash;listed=$taskCount;startUtc=$taskStart;endUtc=[DateTime]::UtcNow.ToString('o');exitCode=$taskExit;expectedResult=$taskPassed} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $PSScriptRoot 'tests-receipt.json') -Encoding utf8
if (-not $taskPassed) { throw 'Standalone normal test gate failed' }
& 'C:/Users/User/Documents/Git/ARCADIA-Studio/.local/scenario-layout-ytr83r-20260913/harness/experiments/scenario-label-measurement-20260913/run-font-version-test.ps1' -TestExecutable $taskExe -ExpectedSha256 $taskHash -RunDirectory (Join-Path $PSScriptRoot 'font-version') -TestName 'scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session'
if ((Get-FileHash -LiteralPath $taskExe -Algorithm SHA256).Hash.ToLowerInvariant() -ne $taskHash) { throw 'Test binary changed during execution' }
@@ -0,0 +1,10 @@
{
"source": "903d85dda403b93731c05a08b97fa7d257d012dd",
"executable": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\scenario-label-gates-attempt3-20260913\\label_tests.exe",
"sha256": "83f33bb63f4b70cbf4c20b39556a3b8e34ef48132928c09a97cebce1a0789e4f",
"listed": 18,
"startUtc": "2026-09-13T01:39:31.0340269Z",
"endUtc": "2026-09-13T01:39:35.4311753Z",
"exitCode": 0,
"expectedResult": true
}
@@ -0,0 +1,23 @@
running 18 tests
test scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session ... ignored, mutates the global font database; run only this exact test in a fresh process
test scenario_label::tests::bounds_translation_rejects_nonfinite_world_positions_and_endpoints ... ok
test scenario_label::tests::adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes ... ok
test scenario_label::tests::empty_and_too_narrow_labels_never_claim_source_visibility ... ok
test scenario_label::tests::finite_scaled_metrics_cannot_escape_as_infinite_world_dimensions ... ok
test scenario_label::tests::malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate ... ok
test scenario_label::tests::grapheme_prefixes_and_zoom_match_actual_paragraph_layout ... ok
test scenario_label::tests::normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation ... ok
test scenario_label::tests::redraws_reuse_resolution_but_width_zoom_and_style_are_dependencies ... ok
test scenario_label::tests::resident_entry_is_remeasured_for_a_fresh_settled_render_session ... ok
test scenario_label::tests::resident_entry_remeasures_after_local_font_epoch_invalidation ... ok
test scenario_label::tests::retained_artifact_rejects_other_zoom_and_render_session_for_every_consumer ... ok
test scenario_label_corridor::tests::caller_defined_self_and_reversed_corridors_keep_their_geometry ... ok
test scenario_label_corridor::tests::complete_padded_band_and_touching_boundaries_are_obstructed ... ok
test scenario_label_corridor::tests::invalid_or_overflowing_geometry_never_enters_a_corridor ... ok
test scenario_label_corridor::tests::long_message_does_not_cross_an_intermediate_execution ... ok
test scenario_label_corridor::tests::overlapping_unsorted_blockers_merge_and_widest_gap_wins ... ok
test scenario_label_corridor::tests::ties_keep_the_nearest_anchor_then_choose_leftmost ... ok
test result: ok. 17 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 2.72s
@@ -0,0 +1,3 @@
attempt1/* -text whitespace=cr-at-eol
attempt2/* -text whitespace=cr-at-eol
attempt3/* -text whitespace=cr-at-eol
@@ -0,0 +1,147 @@
# Scenario label measurement: preserved candidate and source correction
This is an **unwired, uncompiled experiment**, not an accepted renderer policy.
Source base: `208268c8aa7f6bc3c2bab6a3c0657358f3f7dc23`.
Parent's docs-only worktree HEAD: `75cb09dade2cf4b9a176b5e6159f43e2345d196e`.
Only the two new `crates/oa-desktop/src/behavior/scenario_label` module/test
paths and this evidence directory belong to this correction.
## Preserved attempt 1
`attempt1/scenario_label.rs` and `attempt1/tests.rs` are the exact raw files
reviewed before correction. Both SHA256 values were checked before copying and
again after copying; [manifest](attempt1/manifest.json) records source paths and
hashes. They remain historical evidence, not alternate compiler inputs.
Independent static review identified:
1. A retained Label Arc lacked its font epoch and zoom. Cached dimensions could
be reused by painting/hit/navigation after their measurement context changed.
2. Iced's WGPU text cache omits font Version; remeasuring only this module cannot
establish dynamic-font parity with an already-populated renderer cache.
3. The font-epoch test had already evicted its original entry with three variants
in a capacity-two cache. Different Arc identity did not prove invalidation.
4. Finite scaled metrics could overflow when converted back to world units.
Concrete example: size 2, Relative(MAX) line height, zoom 0.5.
These were source findings. Neither the old candidate nor the correction was
compiled or executed by this worker; no failure/pass count is inferred.
## Preserved attempt 2 and missing font-version branch
`attempt2/` preserves both corrected raw source files and this note before the
follow-up, with exact SHA256 checks before and after copying. The root reviewer
found that local cache epoch invalidation and a fresh session identity did not
exercise an actual global font Version change. That specific branch was still
untested; the historical ten tests were not evidence for it.
## Corrected experimental seam
`RenderSession::after_fonts_settled_and_renderer_reset()` explicitly records a
caller-established settled-font rendering lifetime. It does not reset or inspect
the renderer. The caller must finish font loading and create a fresh renderer
cache, then prohibit font changes between resolution, guarded consumption and
draw. A changed global font Version rejects the old session; a new local cache
alone is not a repair for WGPU's old glyph buffers.
Labels record that session identity/font Version and measured zoom. Displayed
text, retained-source bytes, bounds, field retention and canvas Text construction
use checked accessors. Another session, another zoom or a detected font change
rejects a retained artifact. Returned raw Text/bounds must be consumed in that
checked context: holding those values across context changes bypasses the guard.
This is an explicit limited contract, not atomic protection against a concurrent
font load or an incorrect renderer transform supplied by the caller.
Finite world line metrics are checked before shaping. Actual measured dimensions
are checked again after division by zoom. The existing 64KiB source and 4096
grapheme budgets remain explicit experiment errors, not adopted product limits.
Eleven ordinary test candidates now include separate resident-entry font-epoch invalidation,
fresh-session invalidation, stale artifact rejection for every consumer, and
the overflow counterexample, plus finite world-position/rectangle-endpoint
validation for bounds_at and canvas_text. This last check does not establish
validity under arbitrary camera transforms or contain raster ink.
The invalidation assertions require a still-resident
entry, positive prior measurement count and an increased count afterward.
These ordinary tests do not mutate the global font database or claim dynamic-font rendering
acceptance. The existing actual Paragraph comparisons remain layout-metric
oracles, not raster-ink/Scene collision evidence.
One additional **ignored** test, `actual_font_load_rejects_retained_label_and_old_session`,
must run alone in a fresh process. It resolves a label, extracts the actual
Iced-Icons font bytes that pinned Iced embeds in its global fontdb, independently
parses them into a fresh fontdb and confirms the expected family, then loads owned
valid bytes through actual FontSystem::load_font. Both a new face and a changed
Version must be observed. The old Label's five consumer accessors and old-session
Cache::resolve must return FontSetChanged; no new measurements may occur.
This is a valid duplicate-face load, not invalid input used merely to increment
a version. It does not claim the selected face or visual metrics change, create
a WGPU renderer, or test renderer recovery after dynamic loading. No font asset,
system-specific font path, download, or new dependency is needed. Version mutation
is not undone; dedicated process exit is the isolation boundary.
## Pinned primary APIs and remaining evaluator
Cargo.lock pins iced_graphics/iced_wgpu 0.14.0 and iced_widget 0.14.2. Relevant
local registry sources: `iced_wgpu-0.14.0/src/geometry.rs` (`fill_text`, scaled
font metrics), `iced_wgpu-0.14.0/src/text.rs` (`Text::Cached` allocation),
`iced_graphics-0.14.0/src/text/cache.rs` (Key/hash lacks Version), and
`iced_graphics-0.14.0/src/text.rs` (FontSystem::load_font/version).
Independent reviewer confirmed `geometry.rs:306-360`, WGPU `text.rs:443-500`
and its retained BufferCache at line 304 (trim at 435 only evicts unused
entries), graphics `text/cache.rs:109-146`, and graphics `compositor.rs:69`.
The compositor's font-load path changes the global FontSystem, not the WGPU
buffer cache. A check immediately before canvas_text is therefore not atomic
with the later WGPU prepare; fonts must remain unchanged for the entire session.
The module remains undeclared in behavior.rs. Parent may separately add test-only
`#[cfg(test)] mod scenario_label;` and run:
```
cargo test -p oa-desktop --locked --offline behavior::scenario_label::tests:: -- --test-threads=1
```
The normal filter selects twelve tests: eleven ordinary tests and one ignored
global-font test. This is an expected selection, not a claim they pass. Do not run
the whole desktop suite with `--include-ignored` to exercise the font test.
After the parent compiles and records a matching test executable, use the exact
isolation runner (no Cargo invocation inside the runner):
```powershell
& ./harness/experiments/scenario-label-measurement-20260913/run-font-version-test.ps1 `
-TestExecutable $desktopTestExe -ExpectedSha256 $expectedTestHash `
-RunDirectory $freshEvidenceDir
```
It verifies the supplied binary hash, lists exactly one ignored test, then starts
a separate fresh executable process with:
```
--ignored --exact behavior::scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session --nocapture --test-threads=1
```
The runner refuses an existing result directory and requires exactly one passing
test, preserving selection/output/receipt. Parent must provide the matching DLL
environment and source-to-binary provenance. Runner and tests are unexecuted.
It also accepts `-TestName scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session`
for a parent-built standalone test wrapper importing the exact module at crate
root. That result must be reported as standalone Iced/module evidence, not desktop
or native integration. Wrapper, dependency artifacts and source hashes must be
bound separately by the compiler owner.
Corridors remain
external; ordinary/reversed and self-message positioning is not selected here.
The bounded prefix search guarantees measured fit, not a globally maximal prefix
for all contextual shaping. No wrapping, row-height change, public schema or
production gesture is introduced. Real WGPU label ink, activation shoulders,
clipping, viewport transforms, and actual navigation integration remain required
before adopting the seam. Original fixed 32-grapheme/225-unit rendering is still
unchanged for baseline comparison.
## Parent corridor candidate and isolated evaluator
The parent added `scenario_label/corridor.rs`: a bounded geometric candidate that subtracts padded blockers intersecting the full label band and selects the widest connected interval, with deterministic anchor/leftmost tie-breaking. It takes an explicit finite caller-supplied band; self/reversed corridor construction, actual Scene integration, font/ink clearance and release gesture policy remain unimplemented. Six unexecuted tests cover intermediate activations, unsorted overlapping blockers, complete-band intersections, ties, explicit self/reversed bands and invalid/overflow coordinates. This is an evaluation candidate, not an adopted paint policy.
`attempt3` preserves both corrected raw label modules and the first raw corridor before scoped rustfmt. `standalone.rs` imports the actual three current source files without copying their implementation or wiring the desktop. Its expected listing is 18 tests: 17 ordinary and one ignored global-font mutation. Compiling/running that wrapper against explicitly identified pinned Iced/unicode dependencies is a standalone evaluator only; it does not establish desktop integration or WGPU/native acceptance. Use the font runner with the standalone exact test name documented above, in a separate process. No execution result is claimed by this prepared note; subsequent gate evidence will record actual results.
@@ -0,0 +1,17 @@
{
"base": "208268c8aa7f6bc3c2bab6a3c0657358f3f7dc23",
"status": "Original uncompiled, unwired candidate; static review found stale reuse, vacuous epoch test and post-division overflow gaps",
"files": [
{
"path": "crates/oa-desktop/src/behavior/scenario_label.rs",
"hash": "1CACEDE2BCB0E3E86C0D0B691D6E65FDADEA75F93FC74BBAF94E8E2082D6E877",
"name": "scenario_label.rs"
},
{
"path": "crates/oa-desktop/src/behavior/scenario_label/tests.rs",
"hash": "9AC3A0D091E3A319D7822C76B34DCEA56B8D400DBB7F55B3711659E84B6F2BDB",
"name": "tests.rs"
}
],
"docsOnlyHead": "75cb09dade2cf4b9a176b5e6159f43e2345d196e"
}
@@ -0,0 +1,206 @@
//! Unwired single-line scenario label experiment. No renderer or schema is changed.
//!
//! Iced 0.14.0 WGPU `geometry::Frame::fill_text` shapes positive uniform-scale
//! canvas text at `size * zoom`; its `text::prepare` uses graphics text::Cache.
//! This adapter uses that same allocator/font system/attributes, not a character
//! width estimate. Canvas Text::max_width is wrapping, so it stays infinite.
//!
//! Integration seam: after lifecycle/activation endpoint alignment, a caller
//! supplies a usable text width in world units and retains one cache per live
//! labelled occurrence across Scene rebuilds. Paint, hit testing, navigation and extents
//! consume the returned Label. Position/color do not invalidate measurement.
//! This does NOT choose a self-call, reversed-arrow or obstacle corridor policy.
//! It measures layout advances, not raster ink; overhang/clip/activation-border
//! acceptance still requires the real WGPU evaluator. The old 32-grapheme/225px
//! renderer is deliberately untouched for a later baseline comparison.
use iced::advanced::{graphics::text as graphics_text, text::{Alignment, LineHeight, Shaping}};
use iced::{Color, Font, Pixels, Point, Rectangle, Size, widget::canvas::Text};
use std::{collections::VecDeque, ops::Range, sync::Arc};
use unicode_segmentation::UnicodeSegmentation;
const MAX_SOURCE_BYTES: usize = 64 * 1024;
const MAX_GRAPHEMES: usize = 4096;
const MAX_ENTRIES: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Style {
pub font: Font,
pub size: Pixels,
pub line_height: LineHeight,
pub shaping: Shaping,
}
impl Default for Style {
fn default() -> Self {
let text = Text::default();
Self { font: text.font, size: Pixels(12.), line_height: text.line_height, shaping: text.shaping }
}
}
impl Style {
fn scaled(self, zoom: f32) -> (f32, f32) {
let size = Pixels(self.size.0 * zoom);
let line_height = match self.line_height {
LineHeight::Absolute(value) => LineHeight::Absolute(Pixels(value.0 * zoom)),
LineHeight::Relative(value) => LineHeight::Relative(value),
};
(size.0, line_height.to_absolute(size).0)
}
}
#[derive(Clone, Debug)]
pub(super) struct Label {
source: String,
pub displayed: String,
/// Original-label bytes represented before any added ellipsis. Includes
/// collapsed whitespace, but `retains_any` never counts whitespace as text.
pub retained_source: Range<usize>,
/// World-space layout advances, at origin; not glyph-ink containment.
pub bounds: Rectangle,
style: Style,
}
impl Label {
pub fn retains_any(&self, field: Range<usize>) -> bool {
if field.start > field.end || self.source.get(field.clone()).is_none() {
return false;
}
let end = field.end.min(self.retained_source.end);
field.start < end && self.source.get(field.start..end)
.is_some_and(|text| text.chars().any(|c| !c.is_whitespace()))
}
pub fn bounds_at(&self, position: Point) -> Rectangle {
Rectangle::new(position, self.bounds.size())
}
/// Paint under the same positive uniform zoom used for `Cache::resolve`.
/// No per-label wrapping or implicit text maximum is introduced.
pub fn canvas_text(&self, position: Point, color: Color) -> Text {
Text { content: self.displayed.clone(), position, color,
size: self.style.size, line_height: self.style.line_height,
font: self.style.font, shaping: self.style.shaping,
max_width: f32::INFINITY, ..Text::default() }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Error { InvalidMetrics, SourceBudget, GraphemeBudget, FontSystemUnavailable, InvalidMeasurement }
struct Entry { source: String, style: Style, zoom: f32, width: f32, label: Arc<Label> }
#[derive(Default)]
/// Two recent resolutions for ONE live labelled occurrence. A global small LRU
/// would thrash when a larger Scene is rebuilt in order. The integration owner
/// must retain these by occurrence identity and prune on removal/document change.
pub(super) struct Cache {
version: Option<graphics_text::Version>,
entries: VecDeque<Entry>,
#[cfg(test)]
allocations: usize,
}
impl Cache {
/// `width` excludes any caller-chosen backplate padding. Zero is valid.
/// Cache ownership must survive redraws; constructing one per Scene defeats
/// reuse. Fonts loaded through Iced invalidate all entries by Version.
/// Error handling/fallback for these explicit experiment budgets is an
/// integration decision; never silently paint the old truncated contract.
pub fn resolve(&mut self, source: &str, width: f32, zoom: f32, style: Style) -> Result<Arc<Label>, Error> {
let (size, line_height) = style.scaled(zoom);
if !width.is_finite() || width < 0. || !zoom.is_finite() || zoom <= 0.
|| !size.is_finite() || size <= 0. || !line_height.is_finite() || line_height <= 0.
|| !(width * zoom).is_finite() {
return Err(Error::InvalidMetrics);
}
if source.len() > MAX_SOURCE_BYTES { return Err(Error::SourceBudget); }
let mut fonts = graphics_text::font_system().write().map_err(|_| Error::FontSystemUnavailable)?;
if self.version != Some(fonts.version()) {
self.entries.clear();
self.version = Some(fonts.version());
}
if let Some(index) = self.entries.iter().position(|entry|
entry.source == source && entry.style == style && entry.zoom == zoom && entry.width == width) {
let entry = self.entries.remove(index).unwrap();
let label = entry.label.clone();
self.entries.push_back(entry);
return Ok(label);
}
let normalized = Normalized::new(source);
let cuts: Vec<usize> = normalized.text.grapheme_indices(true).map(|(i, _)| i)
.chain(std::iter::once(normalized.text.len())).collect();
if cuts.len().saturating_sub(1) > MAX_GRAPHEMES { return Err(Error::GraphemeBudget); }
// Candidate buffers are temporary. Only the resolved strings/metrics
// survive, bounded by MAX_ENTRIES; renderer caching remains independent.
let mut shapes = graphics_text::Cache::new();
let mut measure = |content: &str| -> Result<Size, Error> {
if content.is_empty() { return Ok(Size::ZERO); }
#[cfg(test)] { self.allocations += 1; }
let (_, entry) = shapes.allocate(fonts.raw(), graphics_text::cache::Key {
content, size, line_height, font: style.font, bounds: Size::INFINITE,
shaping: style.shaping, align_x: Alignment::Default,
});
if entry.buffer.layout_runs().count() != 1 || !entry.min_bounds.width.is_finite()
|| !entry.min_bounds.height.is_finite() { return Err(Error::InvalidMeasurement); }
Ok(Size::new(entry.min_bounds.width / zoom, entry.min_bounds.height / zoom))
};
let full = measure(&normalized.text)?;
let (displayed, retained, measured) = if full.width <= width {
(normalized.text.clone(), normalized.text.len(), full)
} else {
let ellipsis = measure("…")?;
if ellipsis.width > width {
(String::new(), 0, Size::ZERO)
} else {
// Every accepted candidate is actually shaped and fits. Search
// is bounded/logarithmic; contextual shaping is not universally
// monotone, so this does NOT promise the globally longest prefix.
// Full content is always checked first and never needlessly cut.
let mut best = ("…".to_owned(), 0, ellipsis);
let (mut low, mut high) = (1, cuts.len().saturating_sub(1));
while low < high {
let middle = low + (high - low) / 2;
let candidate = format!("{}…", &normalized.text[..cuts[middle]]);
let measured = measure(&candidate)?;
if measured.width <= width {
best = (candidate, cuts[middle], measured);
low = middle + 1;
} else { high = middle; }
}
best
}
};
let label = Arc::new(Label { source: source.into(), displayed,
retained_source: 0..normalized.source_end(retained),
bounds: Rectangle::with_size(measured), style });
if self.entries.len() == MAX_ENTRIES { self.entries.pop_front(); }
self.entries.push_back(Entry { source: source.into(), style, zoom, width, label: label.clone() });
Ok(label)
}
}
struct Normalized { text: String, ends: Vec<(usize, usize)> }
impl Normalized {
fn new(source: &str) -> Self {
let mut result = Self { text: String::new(), ends: vec![(0, 0)] };
let mut separator = false;
for (start, c) in source.char_indices() {
if c.is_whitespace() { separator = !result.text.is_empty(); continue; }
if separator {
result.text.push(' ');
result.ends.push((result.text.len(), start));
separator = false;
}
result.text.push(c);
result.ends.push((result.text.len(), start + c.len_utf8()));
}
result
}
fn source_end(&self, normalized_end: usize) -> usize {
self.ends.binary_search_by_key(&normalized_end, |(end, _)| *end)
.map(|index| self.ends[index].1).expect("grapheme cuts are character boundaries")
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,120 @@
//! Unexecuted evaluator candidates; the module is intentionally not wired yet.
use super::*;
use iced::advanced::{graphics::text::Paragraph, text::{Paragraph as _, Text as ParagraphText, Wrapping}};
// Independent paragraph construction: no candidate cache, normalization, search,
// or character-width estimate is used by this measurement oracle.
fn paragraph_size(content: &str, zoom: f32, style: Style) -> Size {
let size = Pixels(style.size.0 * zoom);
let line_height = match style.line_height {
LineHeight::Relative(factor) => LineHeight::Relative(factor),
LineHeight::Absolute(pixels) => LineHeight::Absolute(Pixels(pixels.0 * zoom)),
};
let paragraph = Paragraph::with_text(ParagraphText { content, bounds: Size::INFINITE,
size, line_height, font: style.font, align_x: Alignment::Default,
align_y: iced::alignment::Vertical::Top, shaping: style.shaping, wrapping: Wrapping::default() });
assert_eq!(paragraph.buffer().layout_runs().count(), 1);
Size::new(paragraph.min_width() / zoom, paragraph.min_height() / zoom)
}
#[test]
fn adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes() {
let mut cache = Cache::default();
let style = Style::default();
for text in [
"1. call · Request valve repair · Technician service conversation",
"2. reply · Return pressure after repair · Technician service conversation",
"1. call · Request redundant hydraulic pressure calibration channel A",
"1. call · Request redundant hydraulic pressure calibration channel B",
] {
let wide = cache.resolve(text, 1600., 1., style).unwrap();
assert_eq!(wide.displayed, text);
assert_eq!(wide.retained_source, 0..text.len());
assert_eq!(wide.bounds.size(), paragraph_size(text, 1., style));
let narrow = cache.resolve(text, 80., 1., style).unwrap();
assert_ne!(narrow.displayed, text);
assert!(narrow.bounds.width <= 80.);
assert_eq!(narrow.bounds.size(), paragraph_size(&narrow.displayed, 1., style));
}
}
#[test]
fn normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation() {
let raw = " \t1. call · Pump\r\n · Operation: Need\u{301}le ";
let expected = "1. call · Pump · Operation: Need\u{301}le";
let n = Normalized::new(raw);
assert_eq!(n.text, expected);
let prefix = "1. call · Pump ";
assert_eq!(n.source_end(prefix.len()), raw.find("· Operation").unwrap());
let mut cache = Cache::default();
let wide = cache.resolve(raw, 1600., 1., Style::default()).unwrap();
assert_eq!(wide.displayed, expected);
assert_eq!(wide.retained_source.end, raw.trim_end().len());
let field = raw.find("Need").unwrap()..raw.trim_end().len();
assert!(wide.retains_any(field.clone()));
assert!(!wide.retains_any(0..2), "normalized whitespace is not a visible field");
let small = cache.resolve(raw, 70., 1., Style::default()).unwrap();
assert!(!small.retains_any(field));
assert!(small.retained_source.end < raw.find("Operation").unwrap());
}
#[test]
fn grapheme_prefixes_and_zoom_match_actual_paragraph_layout() {
let text = "call · 戻り値 圧力 e\u{301} 👩‍🔬 safety valve verification";
let mut cache = Cache::default();
for zoom in [0.66, 1., 1.75] {
let label = cache.resolve(text, 105., zoom, Style::default()).unwrap();
assert!(label.bounds.width <= 105.);
assert_eq!(label.bounds.size(), paragraph_size(&label.displayed, zoom, Style::default()));
assert!(text.grapheme_indices(true).any(|(i, _)| i == label.retained_source.end)
|| label.retained_source.end == text.len());
let painted = label.canvas_text(Point::new(400., 90.), Color::BLACK);
assert_eq!(painted.content, label.displayed);
assert!(painted.max_width.is_infinite());
assert_eq!(label.bounds_at(painted.position).size(), label.bounds.size());
}
}
#[test]
fn empty_and_too_narrow_labels_never_claim_source_visibility() {
let mut cache = Cache::default();
for text in ["", " \t\n", "Operation: Pump"] {
let label = cache.resolve(text, 0., 1., Style::default()).unwrap();
assert_eq!(label.displayed, "");
assert_eq!(label.retained_source, 0..0);
assert_eq!(label.bounds.size(), Size::ZERO);
assert!(!label.retains_any(0..text.len()));
}
}
#[test]
fn redraws_reuse_resolution_but_width_zoom_style_and_font_epoch_are_dependencies() {
let mut cache = Cache::default();
let text = "call · Pressure verification with a descriptive operation label";
let style = Style::default();
let a = cache.resolve(text, 100., 1., style).unwrap();
let measured = cache.allocations;
let b = cache.resolve(text, 100., 1., style).unwrap();
assert!(Arc::ptr_eq(&a, &b));
assert_eq!(cache.allocations, measured);
for (width, zoom, style) in [(120., 1., style), (100., 0.66, style),
(100., 1., Style { size: Pixels(14.), ..style })] {
let other = cache.resolve(text, width, zoom, style).unwrap();
assert!(!Arc::ptr_eq(&a, &other));
}
// Simulate an invalidated local epoch without mutating the shared font DB.
cache.version = None;
let refreshed = cache.resolve(text, 100., 1., style).unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
}
#[test]
fn malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate() {
let mut cache = Cache::default();
for (width, zoom) in [(f32::NAN, 1.), (-1., 1.), (100., 0.), (100., f32::INFINITY)] {
assert_eq!(cache.resolve("call", width, zoom, Style::default()).unwrap_err(), Error::InvalidMetrics);
}
assert_eq!(cache.resolve(&"x".repeat(MAX_SOURCE_BYTES + 1), 100., 1., Style::default()).unwrap_err(), Error::SourceBudget);
assert_eq!(cache.resolve(&"x".repeat(MAX_GRAPHEMES + 1), 100., 1., Style::default()).unwrap_err(), Error::GraphemeBudget);
assert!(cache.entries.is_empty());
}
@@ -0,0 +1,87 @@
# Scenario label measurement: preserved candidate and source correction
This is an **unwired, uncompiled experiment**, not an accepted renderer policy.
Source base: `208268c8aa7f6bc3c2bab6a3c0657358f3f7dc23`.
Parent's docs-only worktree HEAD: `75cb09dade2cf4b9a176b5e6159f43e2345d196e`.
Only the two new `crates/oa-desktop/src/behavior/scenario_label` module/test
paths and this evidence directory belong to this correction.
## Preserved attempt 1
`attempt1/scenario_label.rs` and `attempt1/tests.rs` are the exact raw files
reviewed before correction. Both SHA256 values were checked before copying and
again after copying; [manifest](attempt1/manifest.json) records source paths and
hashes. They remain historical evidence, not alternate compiler inputs.
Independent static review identified:
1. A retained Label Arc lacked its font epoch and zoom. Cached dimensions could
be reused by painting/hit/navigation after their measurement context changed.
2. Iced's WGPU text cache omits font Version; remeasuring only this module cannot
establish dynamic-font parity with an already-populated renderer cache.
3. The font-epoch test had already evicted its original entry with three variants
in a capacity-two cache. Different Arc identity did not prove invalidation.
4. Finite scaled metrics could overflow when converted back to world units.
Concrete example: size 2, Relative(MAX) line height, zoom 0.5.
These were source findings. Neither the old candidate nor the correction was
compiled or executed by this worker; no failure/pass count is inferred.
## Corrected experimental seam
`RenderSession::after_fonts_settled_and_renderer_reset()` explicitly records a
caller-established settled-font rendering lifetime. It does not reset or inspect
the renderer. The caller must finish font loading and create a fresh renderer
cache, then prohibit font changes between resolution, guarded consumption and
draw. A changed global font Version rejects the old session; a new local cache
alone is not a repair for WGPU's old glyph buffers.
Labels record that session identity/font Version and measured zoom. Displayed
text, retained-source bytes, bounds, field retention and canvas Text construction
use checked accessors. Another session, another zoom or a detected font change
rejects a retained artifact. Returned raw Text/bounds must be consumed in that
checked context: holding those values across context changes bypasses the guard.
This is an explicit limited contract, not atomic protection against a concurrent
font load or an incorrect renderer transform supplied by the caller.
Finite world line metrics are checked before shaping. Actual measured dimensions
are checked again after division by zoom. The existing 64KiB source and 4096
grapheme budgets remain explicit experiment errors, not adopted product limits.
Ten test candidates now include separate resident-entry font-epoch invalidation,
fresh-session invalidation, stale artifact rejection for every consumer, and
the overflow counterexample. The invalidation assertions require a still-resident
entry, positive prior measurement count and an increased count afterward.
These tests do not mutate the global font database or claim dynamic-font rendering
acceptance. The existing actual Paragraph comparisons remain layout-metric
oracles, not raster-ink/Scene collision evidence.
## Pinned primary APIs and remaining evaluator
Cargo.lock pins iced_graphics/iced_wgpu 0.14.0 and iced_widget 0.14.2. Relevant
local registry sources: `iced_wgpu-0.14.0/src/geometry.rs` (`fill_text`, scaled
font metrics), `iced_wgpu-0.14.0/src/text.rs` (`Text::Cached` allocation),
`iced_graphics-0.14.0/src/text/cache.rs` (Key/hash lacks Version), and
`iced_graphics-0.14.0/src/text.rs` (FontSystem::load_font/version).
Independent reviewer confirmed `geometry.rs:306-360`, WGPU `text.rs:443-500`
and its retained BufferCache at line 304 (trim at 435 only evicts unused
entries), graphics `text/cache.rs:109-146`, and graphics `compositor.rs:69`.
The compositor's font-load path changes the global FontSystem, not the WGPU
buffer cache. A check immediately before canvas_text is therefore not atomic
with the later WGPU prepare; fonts must remain unchanged for the entire session.
The module remains undeclared in behavior.rs. Parent may separately add test-only
`#[cfg(test)] mod scenario_label;` and run:
```
cargo test -p oa-desktop --locked --offline behavior::scenario_label::tests:: -- --test-threads=1
```
Expected selection is exactly ten tests, not a claim they pass. Corridors remain
external; ordinary/reversed and self-message positioning is not selected here.
The bounded prefix search guarantees measured fit, not a globally maximal prefix
for all contextual shaping. No wrapping, row-height change, public schema or
production gesture is introduced. Real WGPU label ink, activation shoulders,
clipping, viewport transforms, and actual navigation integration remain required
before adopting the seam. Original fixed 32-grapheme/225-unit rendering is still
unchanged for baseline comparison.
@@ -0,0 +1,22 @@
{
"sourceBase": "208268c8aa7f6bc3c2bab6a3c0657358f3f7dc23",
"docsOnlyHead": "75cb09dade2cf4b9a176b5e6159f43e2345d196e",
"files": [
{
"name": "scenario_label.rs",
"hash": "82789CF6C9D0C3F0B027671D13E286F869A359DF1F163D8B47EA1B1E442CAAF4",
"path": "crates/oa-desktop/src/behavior/scenario_label.rs"
},
{
"name": "tests.rs",
"hash": "0C7A0CE80570B8DA347B5147AD00DB9AF26AE4E3FDE0F445683D1958348972AF",
"path": "crates/oa-desktop/src/behavior/scenario_label/tests.rs"
},
{
"name": "README.md",
"hash": "D49F5A48B6C943C4FBA3DB86BFC5C916719DB01A9D962FAB381D976B96E5D993",
"path": "harness/experiments/scenario-label-measurement-20260913/README.md"
}
],
"status": "Uncompiled corrected candidate before actual font-load coverage"
}
@@ -0,0 +1,271 @@
//! Unwired single-line scenario label experiment. No renderer or schema is changed.
//!
//! Iced 0.14.0 WGPU `geometry::Frame::fill_text` shapes positive uniform-scale
//! canvas text at `size * zoom`; its `text::prepare` uses graphics text::Cache.
//! This adapter uses that same allocator/font system/attributes, not a character
//! width estimate. Canvas Text::max_width is wrapping, so it stays infinite.
//! Experimental parity requires settled fonts and a fresh renderer/cache after
//! font loading. Iced's WGPU text cache key omits FontSystem::Version, so merely
//! remeasuring here cannot repair its cached glyph buffers after dynamic loading.
//! A RenderSession is caller attestation of that lifecycle, not its enforcement.
//! No fonts may be loaded between measurement, guarded access and actual draw.
//!
//! Integration seam: after lifecycle/activation endpoint alignment, a caller
//! supplies a usable text width in world units and retains one cache per live
//! labelled occurrence across Scene rebuilds. Paint, hit testing, navigation and extents
//! consume the returned Label. Position/color do not invalidate measurement.
//! This does NOT choose a self-call, reversed-arrow or obstacle corridor policy.
//! It measures layout advances, not raster ink; overhang/clip/activation-border
//! acceptance still requires the real WGPU evaluator. The old 32-grapheme/225px
//! renderer is deliberately untouched for a later baseline comparison.
use iced::advanced::{graphics::text as graphics_text, text::{Alignment, LineHeight, Shaping}};
use iced::{Color, Font, Pixels, Point, Rectangle, Size, widget::canvas::Text};
use std::{collections::VecDeque, ops::Range, sync::Arc};
use unicode_segmentation::UnicodeSegmentation;
const MAX_SOURCE_BYTES: usize = 64 * 1024;
const MAX_GRAPHEMES: usize = 4096;
const MAX_ENTRIES: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Style {
pub font: Font,
pub size: Pixels,
pub line_height: LineHeight,
pub shaping: Shaping,
}
impl Default for Style {
fn default() -> Self {
let text = Text::default();
Self { font: text.font, size: Pixels(12.), line_height: text.line_height, shaping: text.shaping }
}
}
impl Style {
fn scaled(self, zoom: f32) -> (f32, f32) {
let size = Pixels(self.size.0 * zoom);
let line_height = match self.line_height {
LineHeight::Absolute(value) => LineHeight::Absolute(Pixels(value.0 * zoom)),
LineHeight::Relative(value) => LineHeight::Relative(value),
};
(size.0, line_height.to_absolute(size).0)
}
}
/// Explicit experimental contract: the caller finishes loading fonts and starts
/// a fresh renderer/cache BEFORE capturing this session, then keeps fonts fixed.
/// This module cannot inspect or reset the actual renderer's text cache.
#[derive(Clone, Debug)]
pub(super) struct RenderSession {
version: graphics_text::Version,
identity: Arc<()>,
}
impl RenderSession {
pub fn after_fonts_settled_and_renderer_reset() -> Result<Self, Error> {
let fonts = graphics_text::font_system().read().map_err(|_| Error::FontSystemUnavailable)?;
Ok(Self { version: fonts.version(), identity: Arc::new(()) })
}
}
#[derive(Clone, Debug)]
pub(super) struct Label {
source: String,
displayed: String,
/// Original-label bytes represented before any added ellipsis. Includes
/// collapsed whitespace, but `retains_any` never counts whitespace as text.
retained_source: Range<usize>,
/// World-space layout advances, at origin; not glyph-ink containment.
bounds: Rectangle,
style: Style,
measured_session: RenderSession,
measured_zoom: f32,
}
impl Label {
fn check(&self, session: &RenderSession, zoom: f32) -> Result<(), Error> {
if !Arc::ptr_eq(&self.measured_session.identity, &session.identity) {
return Err(Error::StaleRenderSession);
}
if !zoom.is_finite() || zoom != self.measured_zoom { return Err(Error::StaleZoom); }
let fonts = graphics_text::font_system().read().map_err(|_| Error::FontSystemUnavailable)?;
if fonts.version() != self.measured_session.version || fonts.version() != session.version {
return Err(Error::FontSetChanged);
}
Ok(())
}
pub fn displayed(&self, session: &RenderSession, zoom: f32) -> Result<&str, Error> {
self.check(session, zoom)?;
Ok(&self.displayed)
}
pub fn retained_source(&self, session: &RenderSession, zoom: f32) -> Result<Range<usize>, Error> {
self.check(session, zoom)?;
Ok(self.retained_source.clone())
}
pub fn retains_any(&self, session: &RenderSession, zoom: f32, field: Range<usize>) -> Result<bool, Error> {
self.check(session, zoom)?;
if field.start > field.end || self.source.get(field.clone()).is_none() {
return Ok(false);
}
let end = field.end.min(self.retained_source.end);
Ok(field.start < end && self.source.get(field.start..end)
.is_some_and(|text| text.chars().any(|c| !c.is_whitespace())))
}
pub fn bounds_at(&self, session: &RenderSession, zoom: f32, position: Point) -> Result<Rectangle, Error> {
self.check(session, zoom)?;
Ok(Rectangle::new(position, self.bounds.size()))
}
/// Paint under the same positive uniform zoom used for `Cache::resolve`.
/// No per-label wrapping or implicit text maximum is introduced.
/// The returned Text must be consumed immediately in this session/zoom;
/// retaining raw Text past another font load/zoom change bypasses the guard.
pub fn canvas_text(&self, session: &RenderSession, zoom: f32, position: Point, color: Color) -> Result<Text, Error> {
self.check(session, zoom)?;
Ok(Text { content: self.displayed.clone(), position, color,
size: self.style.size, line_height: self.style.line_height,
font: self.style.font, shaping: self.style.shaping,
max_width: f32::INFINITY, ..Text::default() })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Error { InvalidMetrics, SourceBudget, GraphemeBudget, FontSystemUnavailable, InvalidMeasurement, StaleZoom, StaleRenderSession, FontSetChanged }
struct Entry { source: String, style: Style, zoom: f32, width: f32, label: Arc<Label> }
#[derive(Default)]
/// Two recent resolutions for ONE live labelled occurrence. A global small LRU
/// would thrash when a larger Scene is rebuilt in order. The integration owner
/// must retain these by occurrence identity and prune on removal/document change.
pub(super) struct Cache {
version: Option<graphics_text::Version>,
session: Option<Arc<()>>,
entries: VecDeque<Entry>,
#[cfg(test)]
allocations: usize,
}
impl Cache {
/// `width` excludes any caller-chosen backplate padding. Zero is valid.
/// Cache ownership must survive redraws; constructing one per Scene defeats
/// reuse. Changed fonts invalidate the SESSION: the caller must establish
/// a fresh renderer and a new settled session, not just remeasure this cache.
/// Error handling/fallback for these explicit experiment budgets is an
/// integration decision; never silently paint the old truncated contract.
pub fn resolve(&mut self, session: &RenderSession, source: &str, width: f32, zoom: f32, style: Style) -> Result<Arc<Label>, Error> {
let (size, line_height) = style.scaled(zoom);
if !width.is_finite() || width < 0. || !zoom.is_finite() || zoom <= 0.
|| !size.is_finite() || size <= 0. || !line_height.is_finite() || line_height <= 0.
|| !(width * zoom).is_finite() || !(size / zoom).is_finite()
|| !(line_height / zoom).is_finite() {
return Err(Error::InvalidMetrics);
}
if source.len() > MAX_SOURCE_BYTES { return Err(Error::SourceBudget); }
let mut fonts = graphics_text::font_system().write().map_err(|_| Error::FontSystemUnavailable)?;
if session.version != fonts.version() { return Err(Error::FontSetChanged); }
if self.version != Some(fonts.version())
|| self.session.as_ref().is_none_or(|identity| !Arc::ptr_eq(identity, &session.identity)) {
self.entries.clear();
self.version = Some(fonts.version());
self.session = Some(session.identity.clone());
}
if let Some(index) = self.entries.iter().position(|entry|
entry.source == source && entry.style == style && entry.zoom == zoom && entry.width == width) {
let entry = self.entries.remove(index).unwrap();
let label = entry.label.clone();
self.entries.push_back(entry);
return Ok(label);
}
let normalized = Normalized::new(source);
let cuts: Vec<usize> = normalized.text.grapheme_indices(true).map(|(i, _)| i)
.chain(std::iter::once(normalized.text.len())).collect();
if cuts.len().saturating_sub(1) > MAX_GRAPHEMES { return Err(Error::GraphemeBudget); }
// Candidate buffers are temporary. Only the resolved strings/metrics
// survive, bounded by MAX_ENTRIES; renderer caching remains independent.
let mut shapes = graphics_text::Cache::new();
let mut measure = |content: &str| -> Result<Size, Error> {
if content.is_empty() { return Ok(Size::ZERO); }
#[cfg(test)] { self.allocations += 1; }
let (_, entry) = shapes.allocate(fonts.raw(), graphics_text::cache::Key {
content, size, line_height, font: style.font, bounds: Size::INFINITE,
shaping: style.shaping, align_x: Alignment::Default,
});
if entry.buffer.layout_runs().count() != 1 || !entry.min_bounds.width.is_finite()
|| !entry.min_bounds.height.is_finite() { return Err(Error::InvalidMeasurement); }
world_size(entry.min_bounds, zoom)
};
let full = measure(&normalized.text)?;
let (displayed, retained, measured) = if full.width <= width {
(normalized.text.clone(), normalized.text.len(), full)
} else {
let ellipsis = measure("…")?;
if ellipsis.width > width {
(String::new(), 0, Size::ZERO)
} else {
// Every accepted candidate is actually shaped and fits. Search
// is bounded/logarithmic; contextual shaping is not universally
// monotone, so this does NOT promise the globally longest prefix.
// Full content is always checked first and never needlessly cut.
let mut best = ("…".to_owned(), 0, ellipsis);
let (mut low, mut high) = (1, cuts.len().saturating_sub(1));
while low < high {
let middle = low + (high - low) / 2;
let candidate = format!("{}…", &normalized.text[..cuts[middle]]);
let measured = measure(&candidate)?;
if measured.width <= width {
best = (candidate, cuts[middle], measured);
low = middle + 1;
} else { high = middle; }
}
best
}
};
let label = Arc::new(Label { source: source.into(), displayed,
retained_source: 0..normalized.source_end(retained),
bounds: Rectangle::with_size(measured), style,
measured_session: session.clone(), measured_zoom: zoom });
if self.entries.len() == MAX_ENTRIES { self.entries.pop_front(); }
self.entries.push_back(Entry { source: source.into(), style, zoom, width, label: label.clone() });
Ok(label)
}
}
fn world_size(scaled: Size, zoom: f32) -> Result<Size, Error> {
let world = Size::new(scaled.width / zoom, scaled.height / zoom);
if !world.width.is_finite() || !world.height.is_finite() || world.width < 0. || world.height < 0. {
return Err(Error::InvalidMeasurement);
}
Ok(world)
}
struct Normalized { text: String, ends: Vec<(usize, usize)> }
impl Normalized {
fn new(source: &str) -> Self {
let mut result = Self { text: String::new(), ends: vec![(0, 0)] };
let mut separator = false;
for (start, c) in source.char_indices() {
if c.is_whitespace() { separator = !result.text.is_empty(); continue; }
if separator {
result.text.push(' ');
result.ends.push((result.text.len(), start));
separator = false;
}
result.text.push(c);
result.ends.push((result.text.len(), start + c.len_utf8()));
}
result
}
fn source_end(&self, normalized_end: usize) -> usize {
self.ends.binary_search_by_key(&normalized_end, |(end, _)| *end)
.map(|index| self.ends[index].1).expect("grapheme cuts are character boundaries")
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,200 @@
//! Unexecuted evaluator candidates; the module is intentionally not wired yet.
use super::*;
use iced::advanced::{graphics::text::Paragraph, text::{Paragraph as _, Text as ParagraphText, Wrapping}};
// Independent paragraph construction: no candidate cache, normalization, search,
// or character-width estimate is used by this measurement oracle.
fn paragraph_size(content: &str, zoom: f32, style: Style) -> Size {
let size = Pixels(style.size.0 * zoom);
let line_height = match style.line_height {
LineHeight::Relative(factor) => LineHeight::Relative(factor),
LineHeight::Absolute(pixels) => LineHeight::Absolute(Pixels(pixels.0 * zoom)),
};
let paragraph = Paragraph::with_text(ParagraphText { content, bounds: Size::INFINITE,
size, line_height, font: style.font, align_x: Alignment::Default,
align_y: iced::alignment::Vertical::Top, shaping: style.shaping, wrapping: Wrapping::default() });
assert_eq!(paragraph.buffer().layout_runs().count(), 1);
Size::new(paragraph.min_width() / zoom, paragraph.min_height() / zoom)
}
#[test]
fn adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let style = Style::default();
for text in [
"1. call · Request valve repair · Technician service conversation",
"2. reply · Return pressure after repair · Technician service conversation",
"1. call · Request redundant hydraulic pressure calibration channel A",
"1. call · Request redundant hydraulic pressure calibration channel B",
] {
let wide = cache.resolve(&session, text, 1600., 1., style).unwrap();
assert_eq!(wide.displayed(&session, 1.).unwrap(), text);
assert_eq!(wide.retained_source(&session, 1.).unwrap(), 0..text.len());
assert_eq!(wide.bounds.size(), paragraph_size(text, 1., style));
let narrow = cache.resolve(&session, text, 80., 1., style).unwrap();
assert_ne!(narrow.displayed, text);
assert!(narrow.bounds.width <= 80.);
assert_eq!(narrow.bounds.size(), paragraph_size(&narrow.displayed, 1., style));
}
}
#[test]
fn normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation() {
let raw = " \t1. call · Pump\r\n · Operation: Need\u{301}le ";
let expected = "1. call · Pump · Operation: Need\u{301}le";
let n = Normalized::new(raw);
assert_eq!(n.text, expected);
let prefix = "1. call · Pump ";
assert_eq!(n.source_end(prefix.len()), raw.find("· Operation").unwrap());
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let wide = cache.resolve(&session, raw, 1600., 1., Style::default()).unwrap();
assert_eq!(wide.displayed, expected);
assert_eq!(wide.retained_source.end, raw.trim_end().len());
let field = raw.find("Need").unwrap()..raw.trim_end().len();
assert!(wide.retains_any(&session, 1., field.clone()).unwrap());
assert!(!wide.retains_any(&session, 1., 0..2).unwrap(), "normalized whitespace is not a visible field");
let small = cache.resolve(&session, raw, 70., 1., Style::default()).unwrap();
assert!(!small.retains_any(&session, 1., field).unwrap());
assert!(small.retained_source.end < raw.find("Operation").unwrap());
}
#[test]
fn grapheme_prefixes_and_zoom_match_actual_paragraph_layout() {
let text = "call · 戻り値 圧力 e\u{301} 👩‍🔬 safety valve verification";
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for zoom in [0.66, 1., 1.75] {
let label = cache.resolve(&session, text, 105., zoom, Style::default()).unwrap();
assert!(label.bounds.width <= 105.);
assert_eq!(label.bounds.size(), paragraph_size(&label.displayed, zoom, Style::default()));
assert!(text.grapheme_indices(true).any(|(i, _)| i == label.retained_source.end)
|| label.retained_source.end == text.len());
let painted = label.canvas_text(&session, zoom, Point::new(400., 90.), Color::BLACK).unwrap();
assert_eq!(painted.content, label.displayed);
assert!(painted.max_width.is_infinite());
assert_eq!(label.bounds_at(&session, zoom, painted.position).unwrap().size(), label.bounds.size());
}
}
#[test]
fn empty_and_too_narrow_labels_never_claim_source_visibility() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for text in ["", " \t\n", "Operation: Pump"] {
let label = cache.resolve(&session, text, 0., 1., Style::default()).unwrap();
assert_eq!(label.displayed, "");
assert_eq!(label.retained_source, 0..0);
assert_eq!(label.bounds.size(), Size::ZERO);
assert!(!label.retains_any(&session, 1., 0..text.len()).unwrap());
}
}
#[test]
fn redraws_reuse_resolution_but_width_zoom_and_style_are_dependencies() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Pressure verification with a descriptive operation label";
let style = Style::default();
let a = cache.resolve(&session, text, 100., 1., style).unwrap();
let measured = cache.allocations;
let b = cache.resolve(&session, text, 100., 1., style).unwrap();
assert!(Arc::ptr_eq(&a, &b));
assert_eq!(cache.allocations, measured);
for (width, zoom, style) in [(120., 1., style), (100., 0.66, style),
(100., 1., Style { size: Pixels(14.), ..style })] {
let other = cache.resolve(&session, text, width, zoom, style).unwrap();
assert!(!Arc::ptr_eq(&a, &other));
}
}
#[test]
fn resident_entry_is_remeasured_for_a_fresh_settled_render_session() {
let mut cache = Cache::default();
let old_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Pressure verification";
let a = cache.resolve(&old_session, text, 100., 1., Style::default()).unwrap();
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
let measured = cache.allocations;
assert!(measured > 0);
assert!(Arc::ptr_eq(&a, &cache.resolve(&old_session, text, 100., 1., Style::default()).unwrap()));
assert_eq!(cache.allocations, measured);
// A fresh renderer session invalidates a still-resident entry. This does
// not mutate global fonts or purport to test dynamic WGPU font loading.
let new_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let refreshed = cache.resolve(&new_session, text, 100., 1., Style::default()).unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
assert!(cache.allocations > measured);
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &refreshed));
}
#[test]
fn resident_entry_remeasures_after_local_font_epoch_invalidation() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Resident pressure label";
let a = cache.resolve(&session, text, 100., 1., Style::default()).unwrap();
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
let measured = cache.allocations;
assert!(measured > 0);
// Exercise the local memo epoch branch while a is still resident. No
// intervening width/zoom/style resolution can cause incidental eviction.
// This is not an actual font-load or renderer-cache acceptance test.
cache.version = None;
let refreshed = cache.resolve(&session, text, 100., 1., Style::default()).unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
assert!(cache.allocations > measured);
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &refreshed));
}
#[test]
fn retained_artifact_rejects_other_zoom_and_render_session_for_every_consumer() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let label = cache.resolve(&session, "Operation: Pressure", 200., 1., Style::default()).unwrap();
let other = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for (context, zoom, expected) in [(&session, 0.66, Error::StaleZoom), (&other, 1., Error::StaleRenderSession)] {
assert_eq!(label.displayed(context, zoom).unwrap_err(), expected);
assert_eq!(label.retained_source(context, zoom).unwrap_err(), expected);
assert_eq!(label.retains_any(context, zoom, 0..9).unwrap_err(), expected);
assert_eq!(label.bounds_at(context, zoom, Point::ORIGIN).unwrap_err(), expected);
assert_eq!(label.canvas_text(context, zoom, Point::ORIGIN, Color::BLACK).unwrap_err(), expected);
}
assert!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).is_ok());
}
#[test]
fn finite_scaled_metrics_cannot_escape_as_infinite_world_dimensions() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let style = Style { size: Pixels(2.), line_height: LineHeight::Relative(f32::MAX), ..Style::default() };
// At zoom 0.5 the scaled size=1 and line height=MAX are finite. The
// required world line height is not; reject before passing it to shaping.
let scaled = style.scaled(0.5);
assert!(scaled.0.is_finite() && scaled.1.is_finite());
assert_eq!(cache.resolve(&session, "call", 100., 0.5, style).unwrap_err(), Error::InvalidMetrics);
assert_eq!(cache.allocations, 0);
assert!(cache.entries.is_empty());
// Independently exercise validation after division for actual measured
// dimensions: finite scaled width/height do not imply finite world values.
assert_eq!(world_size(Size::new(f32::MAX, 1.), 0.5), Err(Error::InvalidMeasurement));
assert_eq!(world_size(Size::new(1., f32::MAX), 0.5), Err(Error::InvalidMeasurement));
assert_eq!(world_size(Size::new(20., 8.), 0.5).unwrap(), Size::new(40., 16.));
}
#[test]
fn malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for (width, zoom) in [(f32::NAN, 1.), (-1., 1.), (100., 0.), (100., f32::INFINITY)] {
assert_eq!(cache.resolve(&session, "call", width, zoom, Style::default()).unwrap_err(), Error::InvalidMetrics);
}
assert_eq!(cache.resolve(&session, &"x".repeat(MAX_SOURCE_BYTES + 1), 100., 1., Style::default()).unwrap_err(), Error::SourceBudget);
assert_eq!(cache.resolve(&session, &"x".repeat(MAX_GRAPHEMES + 1), 100., 1., Style::default()).unwrap_err(), Error::GraphemeBudget);
assert!(cache.entries.is_empty());
}
@@ -0,0 +1,154 @@
//! Unwired corridor-policy candidate for the measured scenario-label experiment.
//!
//! The caller supplies a finite structural label band AFTER endpoint/activation
//! alignment, with padding already removed from its edges. Blockers include
//! padded activation/card/header bounds over the complete text band, including
//! stroke/ink clearance. This function does not infer a self-message corridor,
//! alter scene extents, change zoom, or establish raster-ink containment.
use iced::Rectangle;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct InvalidGeometry;
fn edges(rect: Rectangle) -> Result<(f32, f32, f32, f32), InvalidGeometry> {
let right = rect.x + rect.width;
let bottom = rect.y + rect.height;
if [rect.x, rect.y, rect.width, rect.height, right, bottom]
.into_iter()
.any(|value| !value.is_finite())
|| rect.width < 0.
|| rect.height < 0.
{
return Err(InvalidGeometry);
}
Ok((rect.x, right, rect.y, bottom))
}
/// Select one connected clear interval; never assemble text across blockers.
/// Prefer greater width, then proximity to the previous label anchor, then the
/// leftmost interval. Input order has no influence. A zero-area band has no
/// corridor. Touching the padded vertical band is conservatively obstructing;
/// a zero-area blocker has no area and must receive stroke padding upstream.
pub(super) fn widest_clear(
band: Rectangle,
blockers: &[Rectangle],
preferred_anchor: f32,
) -> Result<Option<Rectangle>, InvalidGeometry> {
let (left, right, top, bottom) = edges(band)?;
if !preferred_anchor.is_finite() {
return Err(InvalidGeometry);
}
let mut blocked = Vec::new();
for &blocker in blockers {
let (x0, x1, y0, y1) = edges(blocker)?;
if blocker.width > 0. && blocker.height > 0. && y1 >= top && y0 <= bottom {
let interval = (x0.max(left), x1.min(right));
if interval.0 < interval.1 {
blocked.push(interval);
}
}
}
if band.width == 0. || band.height == 0. {
return Ok(None);
}
blocked.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
let mut best: Option<Rectangle> = None;
let mut consider = |start: f32, end: f32| {
if start >= end {
return;
}
let candidate = Rectangle { x: start, width: end - start, ..band };
// Use f64 only for comparison distance: subtracting two admitted f32
// world coordinates can overflow even though each rectangle is finite.
let distance = |x: f32| (f64::from(x) - f64::from(preferred_anchor)).abs();
if best.is_none_or(|old| {
candidate.width > old.width
|| (candidate.width == old.width
&& (distance(candidate.x) < distance(old.x)
|| (distance(candidate.x) == distance(old.x) && candidate.x < old.x)))
}) {
best = Some(candidate);
}
};
let mut cursor = left;
for (start, end) in blocked {
consider(cursor, start);
cursor = cursor.max(end);
}
consider(cursor, right);
Ok(best)
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x: f32, y: f32, width: f32, height: f32) -> Rectangle {
Rectangle { x, y, width, height }
}
#[test]
fn long_message_does_not_cross_an_intermediate_execution() {
let band = rect(20., 100., 700., 22.);
let blockers = [rect(250., 80., 16., 80.), rect(490., 90., 28., 140.)];
let result = widest_clear(band, &blockers, band.x).unwrap().unwrap();
assert_eq!(result, rect(20., 100., 230., 22.));
for blocker in blockers {
assert!(result.x + result.width <= blocker.x || result.x >= blocker.x + blocker.width);
}
assert_eq!(widest_clear(band, &[], band.x).unwrap(), Some(band));
}
#[test]
fn overlapping_unsorted_blockers_merge_and_widest_gap_wins() {
let band = rect(-100., 10., 600., 20.);
let blockers = [rect(50., 0., 80., 60.), rect(0., 5., 70., 30.), rect(450., 0., 100., 60.)];
let expected = Some(rect(130., 10., 320., 20.));
assert_eq!(widest_clear(band, &blockers, -100.).unwrap(), expected);
let mut reversed = blockers;
reversed.reverse();
assert_eq!(widest_clear(band, &reversed, -100.).unwrap(), expected);
}
#[test]
fn complete_padded_band_and_touching_boundaries_are_obstructed() {
let band = rect(0., 100., 200., 20.);
// The first blocker ends at the band top: checking only a baseline
// would miss it. The second starts at the band bottom.
let blockers = [rect(0., 70., 60., 30.), rect(150., 120., 50., 30.)];
assert_eq!(widest_clear(band, &blockers, 0.).unwrap(), Some(rect(60., 100., 90., 20.)));
let outside = [rect(0., 70., 200., 29.), rect(0., 121., 200., 30.)];
assert_eq!(widest_clear(band, &outside, 0.).unwrap(), Some(band));
}
#[test]
fn ties_keep_the_nearest_anchor_then_choose_leftmost() {
let band = rect(0., 0., 300., 20.);
let blockers = [rect(100., 0., 100., 20.)];
assert_eq!(widest_clear(band, &blockers, 190.).unwrap(), Some(rect(200., 0., 100., 20.)));
assert_eq!(widest_clear(band, &blockers, 100.).unwrap(), Some(rect(0., 0., 100., 20.)));
}
#[test]
fn caller_defined_self_and_reversed_corridors_keep_their_geometry() {
// Direction and self-loop policy are explicit caller inputs. Neither
// a zero endpoint gap nor screen width can silently supply a corridor.
for band in [rect(420., 40., 180., 22.), rect(-400., 90., 350., 22.)] {
assert_eq!(widest_clear(band, &[], band.x).unwrap(), Some(band));
}
assert_eq!(widest_clear(rect(420., 40., 0., 22.), &[], 420.).unwrap(), None);
assert_eq!(widest_clear(rect(420., 40., 180., 0.), &[], 420.).unwrap(), None);
assert_eq!(widest_clear(rect(0., 0., 100., 20.), &[rect(-10., -10., 200., 50.)], 0.).unwrap(), None);
}
#[test]
fn invalid_or_overflowing_geometry_never_enters_a_corridor() {
let band = rect(0., 0., 100., 20.);
for invalid in [rect(f32::NAN, 0., 10., 20.), rect(0., 0., -1., 20.),
rect(f32::MAX, 0., f32::MAX, 20.), rect(0., f32::MAX, 10., f32::MAX)] {
assert_eq!(widest_clear(invalid, &[], 0.), Err(InvalidGeometry));
assert_eq!(widest_clear(band, &[invalid], 0.), Err(InvalidGeometry));
}
assert_eq!(widest_clear(band, &[], f32::INFINITY), Err(InvalidGeometry));
}
}
@@ -0,0 +1,17 @@
[
{
"name": "scenario_label.rs",
"bytes": 13548,
"sha256": "b452249c45b1225760d1b9bd8f2d9241c4d599e8a68d7328641d61381c28cbbd"
},
{
"name": "tests.rs",
"bytes": 15683,
"sha256": "1af0115850ae826f3af211499006c26bd290c17cc186d08b41be0ee40981a83b"
},
{
"name": "corridor.rs",
"bytes": 6657,
"sha256": "94747ab1b161564c8901f3d0702034e43f14f263decd8e7b823c801fbcb80dd7"
}
]
@@ -0,0 +1,281 @@
//! Unwired single-line scenario label experiment. No renderer or schema is changed.
//!
//! Iced 0.14.0 WGPU `geometry::Frame::fill_text` shapes positive uniform-scale
//! canvas text at `size * zoom`; its `text::prepare` uses graphics text::Cache.
//! This adapter uses that same allocator/font system/attributes, not a character
//! width estimate. Canvas Text::max_width is wrapping, so it stays infinite.
//! Experimental parity requires settled fonts and a fresh renderer/cache after
//! font loading. Iced's WGPU text cache key omits FontSystem::Version, so merely
//! remeasuring here cannot repair its cached glyph buffers after dynamic loading.
//! A RenderSession is caller attestation of that lifecycle, not its enforcement.
//! No fonts may be loaded between measurement, guarded access and actual draw.
//!
//! Integration seam: after lifecycle/activation endpoint alignment, a caller
//! supplies a usable text width in world units and retains one cache per live
//! labelled occurrence across Scene rebuilds. Paint, hit testing, navigation and extents
//! consume the returned Label. Position/color do not invalidate measurement.
//! This does NOT choose a self-call, reversed-arrow or obstacle corridor policy.
//! It measures layout advances, not raster ink; overhang/clip/activation-border
//! acceptance still requires the real WGPU evaluator. The old 32-grapheme/225px
//! renderer is deliberately untouched for a later baseline comparison.
use iced::advanced::{graphics::text as graphics_text, text::{Alignment, LineHeight, Shaping}};
use iced::{Color, Font, Pixels, Point, Rectangle, Size, widget::canvas::Text};
use std::{collections::VecDeque, ops::Range, sync::Arc};
use unicode_segmentation::UnicodeSegmentation;
const MAX_SOURCE_BYTES: usize = 64 * 1024;
const MAX_GRAPHEMES: usize = 4096;
const MAX_ENTRIES: usize = 2;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Style {
pub font: Font,
pub size: Pixels,
pub line_height: LineHeight,
pub shaping: Shaping,
}
impl Default for Style {
fn default() -> Self {
let text = Text::default();
Self { font: text.font, size: Pixels(12.), line_height: text.line_height, shaping: text.shaping }
}
}
impl Style {
fn scaled(self, zoom: f32) -> (f32, f32) {
let size = Pixels(self.size.0 * zoom);
let line_height = match self.line_height {
LineHeight::Absolute(value) => LineHeight::Absolute(Pixels(value.0 * zoom)),
LineHeight::Relative(value) => LineHeight::Relative(value),
};
(size.0, line_height.to_absolute(size).0)
}
}
/// Explicit experimental contract: the caller finishes loading fonts and starts
/// a fresh renderer/cache BEFORE capturing this session, then keeps fonts fixed.
/// This module cannot inspect or reset the actual renderer's text cache.
#[derive(Clone, Debug)]
pub(super) struct RenderSession {
version: graphics_text::Version,
identity: Arc<()>,
}
impl RenderSession {
pub fn after_fonts_settled_and_renderer_reset() -> Result<Self, Error> {
let fonts = graphics_text::font_system().read().map_err(|_| Error::FontSystemUnavailable)?;
Ok(Self { version: fonts.version(), identity: Arc::new(()) })
}
}
#[derive(Clone, Debug)]
pub(super) struct Label {
source: String,
displayed: String,
/// Original-label bytes represented before any added ellipsis. Includes
/// collapsed whitespace, but `retains_any` never counts whitespace as text.
retained_source: Range<usize>,
/// World-space layout advances, at origin; not glyph-ink containment.
bounds: Rectangle,
style: Style,
measured_session: RenderSession,
measured_zoom: f32,
}
impl Label {
fn check(&self, session: &RenderSession, zoom: f32) -> Result<(), Error> {
if !Arc::ptr_eq(&self.measured_session.identity, &session.identity) {
return Err(Error::StaleRenderSession);
}
if !zoom.is_finite() || zoom != self.measured_zoom { return Err(Error::StaleZoom); }
let fonts = graphics_text::font_system().read().map_err(|_| Error::FontSystemUnavailable)?;
if fonts.version() != self.measured_session.version || fonts.version() != session.version {
return Err(Error::FontSetChanged);
}
Ok(())
}
pub fn displayed(&self, session: &RenderSession, zoom: f32) -> Result<&str, Error> {
self.check(session, zoom)?;
Ok(&self.displayed)
}
pub fn retained_source(&self, session: &RenderSession, zoom: f32) -> Result<Range<usize>, Error> {
self.check(session, zoom)?;
Ok(self.retained_source.clone())
}
pub fn retains_any(&self, session: &RenderSession, zoom: f32, field: Range<usize>) -> Result<bool, Error> {
self.check(session, zoom)?;
if field.start > field.end || self.source.get(field.clone()).is_none() {
return Ok(false);
}
let end = field.end.min(self.retained_source.end);
Ok(field.start < end && self.source.get(field.start..end)
.is_some_and(|text| text.chars().any(|c| !c.is_whitespace())))
}
/// Validate finite world position and rectangle endpoints only. Camera
/// transforms and raster bounds remain a separate integration contract.
pub fn bounds_at(&self, session: &RenderSession, zoom: f32, position: Point) -> Result<Rectangle, Error> {
self.check(session, zoom)?;
translated_bounds(position, self.bounds.size())
}
/// Paint under the same positive uniform zoom used for `Cache::resolve`.
/// No per-label wrapping or implicit text maximum is introduced.
/// The returned Text must be consumed immediately in this session/zoom;
/// retaining raw Text past another font load/zoom change bypasses the guard.
pub fn canvas_text(&self, session: &RenderSession, zoom: f32, position: Point, color: Color) -> Result<Text, Error> {
self.bounds_at(session, zoom, position)?;
Ok(Text { content: self.displayed.clone(), position, color,
size: self.style.size, line_height: self.style.line_height,
font: self.style.font, shaping: self.style.shaping,
max_width: f32::INFINITY, ..Text::default() })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Error { InvalidMetrics, SourceBudget, GraphemeBudget, FontSystemUnavailable, InvalidMeasurement, InvalidPosition, StaleZoom, StaleRenderSession, FontSetChanged }
struct Entry { source: String, style: Style, zoom: f32, width: f32, label: Arc<Label> }
#[derive(Default)]
/// Two recent resolutions for ONE live labelled occurrence. A global small LRU
/// would thrash when a larger Scene is rebuilt in order. The integration owner
/// must retain these by occurrence identity and prune on removal/document change.
pub(super) struct Cache {
version: Option<graphics_text::Version>,
session: Option<Arc<()>>,
entries: VecDeque<Entry>,
#[cfg(test)]
allocations: usize,
}
impl Cache {
/// `width` excludes any caller-chosen backplate padding. Zero is valid.
/// Cache ownership must survive redraws; constructing one per Scene defeats
/// reuse. Changed fonts invalidate the SESSION: the caller must establish
/// a fresh renderer and a new settled session, not just remeasure this cache.
/// Error handling/fallback for these explicit experiment budgets is an
/// integration decision; never silently paint the old truncated contract.
pub fn resolve(&mut self, session: &RenderSession, source: &str, width: f32, zoom: f32, style: Style) -> Result<Arc<Label>, Error> {
let (size, line_height) = style.scaled(zoom);
if !width.is_finite() || width < 0. || !zoom.is_finite() || zoom <= 0.
|| !size.is_finite() || size <= 0. || !line_height.is_finite() || line_height <= 0.
|| !(width * zoom).is_finite() || !(size / zoom).is_finite()
|| !(line_height / zoom).is_finite() {
return Err(Error::InvalidMetrics);
}
if source.len() > MAX_SOURCE_BYTES { return Err(Error::SourceBudget); }
let mut fonts = graphics_text::font_system().write().map_err(|_| Error::FontSystemUnavailable)?;
if session.version != fonts.version() { return Err(Error::FontSetChanged); }
if self.version != Some(fonts.version())
|| self.session.as_ref().is_none_or(|identity| !Arc::ptr_eq(identity, &session.identity)) {
self.entries.clear();
self.version = Some(fonts.version());
self.session = Some(session.identity.clone());
}
if let Some(index) = self.entries.iter().position(|entry|
entry.source == source && entry.style == style && entry.zoom == zoom && entry.width == width) {
let entry = self.entries.remove(index).unwrap();
let label = entry.label.clone();
self.entries.push_back(entry);
return Ok(label);
}
let normalized = Normalized::new(source);
let cuts: Vec<usize> = normalized.text.grapheme_indices(true).map(|(i, _)| i)
.chain(std::iter::once(normalized.text.len())).collect();
if cuts.len().saturating_sub(1) > MAX_GRAPHEMES { return Err(Error::GraphemeBudget); }
// Candidate buffers are temporary. Only the resolved strings/metrics
// survive, bounded by MAX_ENTRIES; renderer caching remains independent.
let mut shapes = graphics_text::Cache::new();
let mut measure = |content: &str| -> Result<Size, Error> {
if content.is_empty() { return Ok(Size::ZERO); }
#[cfg(test)] { self.allocations += 1; }
let (_, entry) = shapes.allocate(fonts.raw(), graphics_text::cache::Key {
content, size, line_height, font: style.font, bounds: Size::INFINITE,
shaping: style.shaping, align_x: Alignment::Default,
});
if entry.buffer.layout_runs().count() != 1 || !entry.min_bounds.width.is_finite()
|| !entry.min_bounds.height.is_finite() { return Err(Error::InvalidMeasurement); }
world_size(entry.min_bounds, zoom)
};
let full = measure(&normalized.text)?;
let (displayed, retained, measured) = if full.width <= width {
(normalized.text.clone(), normalized.text.len(), full)
} else {
let ellipsis = measure("…")?;
if ellipsis.width > width {
(String::new(), 0, Size::ZERO)
} else {
// Every accepted candidate is actually shaped and fits. Search
// is bounded/logarithmic; contextual shaping is not universally
// monotone, so this does NOT promise the globally longest prefix.
// Full content is always checked first and never needlessly cut.
let mut best = ("…".to_owned(), 0, ellipsis);
let (mut low, mut high) = (1, cuts.len().saturating_sub(1));
while low < high {
let middle = low + (high - low) / 2;
let candidate = format!("{}…", &normalized.text[..cuts[middle]]);
let measured = measure(&candidate)?;
if measured.width <= width {
best = (candidate, cuts[middle], measured);
low = middle + 1;
} else { high = middle; }
}
best
}
};
let label = Arc::new(Label { source: source.into(), displayed,
retained_source: 0..normalized.source_end(retained),
bounds: Rectangle::with_size(measured), style,
measured_session: session.clone(), measured_zoom: zoom });
if self.entries.len() == MAX_ENTRIES { self.entries.pop_front(); }
self.entries.push_back(Entry { source: source.into(), style, zoom, width, label: label.clone() });
Ok(label)
}
}
fn world_size(scaled: Size, zoom: f32) -> Result<Size, Error> {
let world = Size::new(scaled.width / zoom, scaled.height / zoom);
if !world.width.is_finite() || !world.height.is_finite() || world.width < 0. || world.height < 0. {
return Err(Error::InvalidMeasurement);
}
Ok(world)
}
fn translated_bounds(position: Point, size: Size) -> Result<Rectangle, Error> {
if !position.x.is_finite() || !position.y.is_finite()
|| !(position.x + size.width).is_finite() || !(position.y + size.height).is_finite() {
return Err(Error::InvalidPosition);
}
Ok(Rectangle::new(position, size))
}
struct Normalized { text: String, ends: Vec<(usize, usize)> }
impl Normalized {
fn new(source: &str) -> Self {
let mut result = Self { text: String::new(), ends: vec![(0, 0)] };
let mut separator = false;
for (start, c) in source.char_indices() {
if c.is_whitespace() { separator = !result.text.is_empty(); continue; }
if separator {
result.text.push(' ');
result.ends.push((result.text.len(), start));
separator = false;
}
result.text.push(c);
result.ends.push((result.text.len(), start + c.len_utf8()));
}
result
}
fn source_end(&self, normalized_end: usize) -> usize {
self.ends.binary_search_by_key(&normalized_end, |(end, _)| *end)
.map(|index| self.ends[index].1).expect("grapheme cuts are character boundaries")
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,271 @@
//! Unexecuted evaluator candidates; the module is intentionally not wired yet.
use super::*;
use iced::advanced::{graphics::text::Paragraph, text::{Paragraph as _, Text as ParagraphText, Wrapping}};
// Independent paragraph construction: no candidate cache, normalization, search,
// or character-width estimate is used by this measurement oracle.
fn paragraph_size(content: &str, zoom: f32, style: Style) -> Size {
let size = Pixels(style.size.0 * zoom);
let line_height = match style.line_height {
LineHeight::Relative(factor) => LineHeight::Relative(factor),
LineHeight::Absolute(pixels) => LineHeight::Absolute(Pixels(pixels.0 * zoom)),
};
let paragraph = Paragraph::with_text(ParagraphText { content, bounds: Size::INFINITE,
size, line_height, font: style.font, align_x: Alignment::Default,
align_y: iced::alignment::Vertical::Top, shaping: style.shaping, wrapping: Wrapping::default() });
assert_eq!(paragraph.buffer().layout_runs().count(), 1);
Size::new(paragraph.min_width() / zoom, paragraph.min_height() / zoom)
}
#[test]
fn adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let style = Style::default();
for text in [
"1. call · Request valve repair · Technician service conversation",
"2. reply · Return pressure after repair · Technician service conversation",
"1. call · Request redundant hydraulic pressure calibration channel A",
"1. call · Request redundant hydraulic pressure calibration channel B",
] {
let wide = cache.resolve(&session, text, 1600., 1., style).unwrap();
assert_eq!(wide.displayed(&session, 1.).unwrap(), text);
assert_eq!(wide.retained_source(&session, 1.).unwrap(), 0..text.len());
assert_eq!(wide.bounds.size(), paragraph_size(text, 1., style));
let narrow = cache.resolve(&session, text, 80., 1., style).unwrap();
assert_ne!(narrow.displayed, text);
assert!(narrow.bounds.width <= 80.);
assert_eq!(narrow.bounds.size(), paragraph_size(&narrow.displayed, 1., style));
}
}
#[test]
fn normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation() {
let raw = " \t1. call · Pump\r\n · Operation: Need\u{301}le ";
let expected = "1. call · Pump · Operation: Need\u{301}le";
let n = Normalized::new(raw);
assert_eq!(n.text, expected);
let prefix = "1. call · Pump ";
assert_eq!(n.source_end(prefix.len()), raw.find("· Operation").unwrap());
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let wide = cache.resolve(&session, raw, 1600., 1., Style::default()).unwrap();
assert_eq!(wide.displayed, expected);
assert_eq!(wide.retained_source.end, raw.trim_end().len());
let field = raw.find("Need").unwrap()..raw.trim_end().len();
assert!(wide.retains_any(&session, 1., field.clone()).unwrap());
assert!(!wide.retains_any(&session, 1., 0..2).unwrap(), "normalized whitespace is not a visible field");
let small = cache.resolve(&session, raw, 70., 1., Style::default()).unwrap();
assert!(!small.retains_any(&session, 1., field).unwrap());
assert!(small.retained_source.end < raw.find("Operation").unwrap());
}
#[test]
fn grapheme_prefixes_and_zoom_match_actual_paragraph_layout() {
let text = "call · 戻り値 圧力 e\u{301} 👩‍🔬 safety valve verification";
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for zoom in [0.66, 1., 1.75] {
let label = cache.resolve(&session, text, 105., zoom, Style::default()).unwrap();
assert!(label.bounds.width <= 105.);
assert_eq!(label.bounds.size(), paragraph_size(&label.displayed, zoom, Style::default()));
assert!(text.grapheme_indices(true).any(|(i, _)| i == label.retained_source.end)
|| label.retained_source.end == text.len());
let painted = label.canvas_text(&session, zoom, Point::new(400., 90.), Color::BLACK).unwrap();
assert_eq!(painted.content, label.displayed);
assert!(painted.max_width.is_infinite());
assert_eq!(label.bounds_at(&session, zoom, painted.position).unwrap().size(), label.bounds.size());
}
}
#[test]
fn empty_and_too_narrow_labels_never_claim_source_visibility() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for text in ["", " \t\n", "Operation: Pump"] {
let label = cache.resolve(&session, text, 0., 1., Style::default()).unwrap();
assert_eq!(label.displayed, "");
assert_eq!(label.retained_source, 0..0);
assert_eq!(label.bounds.size(), Size::ZERO);
assert!(!label.retains_any(&session, 1., 0..text.len()).unwrap());
}
}
#[test]
fn redraws_reuse_resolution_but_width_zoom_and_style_are_dependencies() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Pressure verification with a descriptive operation label";
let style = Style::default();
let a = cache.resolve(&session, text, 100., 1., style).unwrap();
let measured = cache.allocations;
let b = cache.resolve(&session, text, 100., 1., style).unwrap();
assert!(Arc::ptr_eq(&a, &b));
assert_eq!(cache.allocations, measured);
for (width, zoom, style) in [(120., 1., style), (100., 0.66, style),
(100., 1., Style { size: Pixels(14.), ..style })] {
let other = cache.resolve(&session, text, width, zoom, style).unwrap();
assert!(!Arc::ptr_eq(&a, &other));
}
}
#[test]
fn resident_entry_is_remeasured_for_a_fresh_settled_render_session() {
let mut cache = Cache::default();
let old_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Pressure verification";
let a = cache.resolve(&old_session, text, 100., 1., Style::default()).unwrap();
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
let measured = cache.allocations;
assert!(measured > 0);
assert!(Arc::ptr_eq(&a, &cache.resolve(&old_session, text, 100., 1., Style::default()).unwrap()));
assert_eq!(cache.allocations, measured);
// A fresh renderer session invalidates a still-resident entry. This does
// not mutate global fonts or purport to test dynamic WGPU font loading.
let new_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let refreshed = cache.resolve(&new_session, text, 100., 1., Style::default()).unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
assert!(cache.allocations > measured);
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &refreshed));
}
#[test]
fn resident_entry_remeasures_after_local_font_epoch_invalidation() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let text = "call · Resident pressure label";
let a = cache.resolve(&session, text, 100., 1., Style::default()).unwrap();
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
let measured = cache.allocations;
assert!(measured > 0);
// Exercise the local memo epoch branch while a is still resident. No
// intervening width/zoom/style resolution can cause incidental eviction.
// This is not an actual font-load or renderer-cache acceptance test.
cache.version = None;
let refreshed = cache.resolve(&session, text, 100., 1., Style::default()).unwrap();
assert!(!Arc::ptr_eq(&a, &refreshed));
assert!(cache.allocations > measured);
assert_eq!(cache.entries.len(), 1);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &refreshed));
}
#[test]
fn retained_artifact_rejects_other_zoom_and_render_session_for_every_consumer() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let label = cache.resolve(&session, "Operation: Pressure", 200., 1., Style::default()).unwrap();
let other = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for (context, zoom, expected) in [(&session, 0.66, Error::StaleZoom), (&other, 1., Error::StaleRenderSession)] {
assert_eq!(label.displayed(context, zoom).unwrap_err(), expected);
assert_eq!(label.retained_source(context, zoom).unwrap_err(), expected);
assert_eq!(label.retains_any(context, zoom, 0..9).unwrap_err(), expected);
assert_eq!(label.bounds_at(context, zoom, Point::ORIGIN).unwrap_err(), expected);
assert_eq!(label.canvas_text(context, zoom, Point::ORIGIN, Color::BLACK).unwrap_err(), expected);
}
assert!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).is_ok());
}
#[test]
fn finite_scaled_metrics_cannot_escape_as_infinite_world_dimensions() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let style = Style { size: Pixels(2.), line_height: LineHeight::Relative(f32::MAX), ..Style::default() };
// At zoom 0.5 the scaled size=1 and line height=MAX are finite. The
// required world line height is not; reject before passing it to shaping.
let scaled = style.scaled(0.5);
assert!(scaled.0.is_finite() && scaled.1.is_finite());
assert_eq!(cache.resolve(&session, "call", 100., 0.5, style).unwrap_err(), Error::InvalidMetrics);
assert_eq!(cache.allocations, 0);
assert!(cache.entries.is_empty());
// Independently exercise validation after division for actual measured
// dimensions: finite scaled width/height do not imply finite world values.
assert_eq!(world_size(Size::new(f32::MAX, 1.), 0.5), Err(Error::InvalidMeasurement));
assert_eq!(world_size(Size::new(1., f32::MAX), 0.5), Err(Error::InvalidMeasurement));
assert_eq!(world_size(Size::new(20., 8.), 0.5).unwrap(), Size::new(40., 16.));
}
#[test]
fn malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
for (width, zoom) in [(f32::NAN, 1.), (-1., 1.), (100., 0.), (100., f32::INFINITY)] {
assert_eq!(cache.resolve(&session, "call", width, zoom, Style::default()).unwrap_err(), Error::InvalidMetrics);
}
assert_eq!(cache.resolve(&session, &"x".repeat(MAX_SOURCE_BYTES + 1), 100., 1., Style::default()).unwrap_err(), Error::SourceBudget);
assert_eq!(cache.resolve(&session, &"x".repeat(MAX_GRAPHEMES + 1), 100., 1., Style::default()).unwrap_err(), Error::GraphemeBudget);
assert!(cache.entries.is_empty());
}
#[test]
fn bounds_translation_rejects_nonfinite_world_positions_and_endpoints() {
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let label = cache.resolve(&session, "call", 100., 1., Style::default()).unwrap();
for position in [Point::new(f32::NAN, 0.), Point::new(0., f32::INFINITY)] {
assert_eq!(label.bounds_at(&session, 1., position).unwrap_err(), Error::InvalidPosition);
assert_eq!(label.canvas_text(&session, 1., position, Color::BLACK).unwrap_err(), Error::InvalidPosition);
}
assert_eq!(translated_bounds(Point::new(f32::MAX, 0.), Size::new(f32::MAX, 1.)), Err(Error::InvalidPosition));
assert_eq!(translated_bounds(Point::new(0., f32::MAX), Size::new(1., f32::MAX)), Err(Error::InvalidPosition));
assert_eq!(label.bounds_at(&session, 1., Point::new(-20., -10.)).unwrap().position(), Point::new(-20., -10.));
}
/// Must run as the ONLY selected test in a dedicated, fresh test process.
/// The ignored marker keeps this global-font mutation out of normal parallel
/// desktop tests. See the evidence directory's exact executable runner.
#[test]
#[ignore = "mutates the global font database; run only this exact test in a fresh process"]
fn actual_font_load_rejects_retained_label_and_old_session() {
use std::borrow::Cow;
use graphics_text::cosmic_text::fontdb;
let mut cache = Cache::default();
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let source = "Operation: Pressure";
let label = cache.resolve(&session, source, 200., 1., Style::default()).unwrap();
assert_eq!(label.displayed(&session, 1.).unwrap(), source);
assert!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).is_ok());
let measured = cache.allocations;
assert!(measured > 0);
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &label));
let (version_before, version_after, faces_before, faces_after) = {
let mut fonts = graphics_text::font_system().write().unwrap();
let face = fonts.raw().db().faces()
.find(|face| face.families.iter().any(|(family, _)| family == "Iced-Icons"))
.expect("Iced 0.14 embeds and loads its known Iced-Icons.ttf face").id;
let bytes = fonts.raw().db().with_face_data(face, |data, _| data.to_vec())
.expect("the built-in binary font has accessible bytes");
assert!(!bytes.is_empty());
// load_font increments Version even for invalid bytes. Independently
// establish actual face admission so this cannot pass with invalid data.
let mut probe = fontdb::Database::new();
probe.load_font_data(bytes.clone());
let admitted = probe.faces().filter(|face|
face.families.iter().any(|(family, _)| family == "Iced-Icons")).count();
assert!(admitted > 0, "the extracted bytes parse as the expected real font");
let before = fonts.version();
assert_eq!(before, session.version);
let count = fonts.raw().db().faces().count();
// Owned bytes intentionally bypass Borrowed-address deduplication.
// This is an actual valid duplicate-face load, not a claim the chosen
// face or rendered metrics change; the stale-epoch rule is the oracle.
fonts.load_font(Cow::Owned(bytes));
(before, fonts.version(), count, fonts.raw().db().faces().count())
};
assert_ne!(version_before, version_after);
assert!(faces_after > faces_before, "actual font admission must accompany the version increment");
assert_eq!(label.displayed(&session, 1.).unwrap_err(), Error::FontSetChanged);
assert_eq!(label.retained_source(&session, 1.).unwrap_err(), Error::FontSetChanged);
assert_eq!(label.retains_any(&session, 1., 0..9).unwrap_err(), Error::FontSetChanged);
assert_eq!(label.bounds_at(&session, 1., Point::ORIGIN).unwrap_err(), Error::FontSetChanged);
assert_eq!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).unwrap_err(), Error::FontSetChanged);
assert_eq!(cache.resolve(&session, source, 200., 1., Style::default()).unwrap_err(), Error::FontSetChanged);
assert_eq!(cache.allocations, measured, "an invalid session must not remeasure and imply renderer repair");
println!("Valid Iced-Icons load: faces {faces_before}->{faces_after}, font Version {version_before:?}->{version_after:?}; old label and session rejected");
// Do not create a fresh session or claim dynamic WGPU cache recovery here.
// Process exit is the isolation boundary; no other test may run afterward.
}
@@ -0,0 +1,50 @@
# Runs an ALREADY BUILT desktop test executable. This script never invokes Cargo.
# Supply the matching runtime DLL environment prepared by the compiler owner.
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$TestExecutable,
[Parameter(Mandatory=$true)][ValidatePattern('^[0-9a-fA-F]{64}$')][string]$ExpectedSha256,
[Parameter(Mandatory=$true)][string]$RunDirectory,
[ValidateSet('behavior::scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session', 'scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session')]
[string]$TestName = 'behavior::scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session'
)
$ErrorActionPreference = 'Stop'
$fontTestName = $TestName
$fontTestExe = (Resolve-Path -LiteralPath $TestExecutable).Path
$fontTestHash = (Get-FileHash -LiteralPath $fontTestExe -Algorithm SHA256).Hash
if ($fontTestHash -ne $ExpectedSha256) { throw 'Test executable SHA256 differs from the supplied compiler receipt' }
if (Test-Path -LiteralPath $RunDirectory) { throw 'Use a fresh evidence directory; refusing to overwrite a previous run' }
$fontTestOutput = (New-Item -ItemType Directory -Path $RunDirectory).FullName
# Listing is a separate process and runs no test. Refuse zero/multiple matches
# before launching the one process that will mutate global font state.
$fontTestList = & $fontTestExe --list --ignored --exact $fontTestName 2>&1
$fontTestListExit = $LASTEXITCODE
$fontTestListText = $fontTestList | Out-String
$fontTestListText | Set-Content -LiteralPath (Join-Path $fontTestOutput 'selection.log') -Encoding utf8
$fontTestSelected = $fontTestListExit -eq 0 -and
$fontTestListText -match ('(?m)^' + [regex]::Escape($fontTestName) + ': test\s*$') -and
$fontTestListText -match '(?m)^1 test, 0 benchmarks\s*$'
if (-not $fontTestSelected) { throw 'The exact ignored-test selection did not list exactly one test; inspect selection.log' }
# Fresh dedicated executable invocation; no normal/other ignored test is chosen.
$fontTestStarted = (Get-Date).ToUniversalTime().ToString('o')
$fontTestRun = & $fontTestExe --ignored --exact $fontTestName --nocapture --test-threads=1 2>&1
$fontTestExit = $LASTEXITCODE
$fontTestRunText = $fontTestRun | Out-String
$fontTestRunText | Set-Content -LiteralPath (Join-Path $fontTestOutput 'test.log') -Encoding utf8
$fontTestPassed = $fontTestExit -eq 0 -and
$fontTestRunText -match 'test result: ok\. 1 passed; 0 failed; 0 ignored;'
@{
test = $fontTestName
executable = $fontTestExe
sha256 = $fontTestHash
exactSelection = $fontTestSelected
startedUtc = $fontTestStarted
finishedUtc = (Get-Date).ToUniversalTime().ToString('o')
exitCode = $fontTestExit
passedExactlyOne = $fontTestPassed
scope = 'Actual global font Version invalidation and artifact rejection; no WGPU dynamic cache recovery claim'
} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $fontTestOutput 'receipt.json') -Encoding utf8
Write-Output $fontTestRunText
if (-not $fontTestPassed) { throw 'Isolated font-version test did not pass exactly one test; inspect test.log and receipt.json' }
@@ -0,0 +1,5 @@
//! Isolated evaluator only: does not wire the desktop renderer.
#[path = "../../../crates/oa-desktop/src/behavior/scenario_label.rs"]
mod scenario_label;
#[path = "../../../crates/oa-desktop/src/behavior/scenario_label/corridor.rs"]
mod scenario_label_corridor;
@@ -0,0 +1,183 @@
# Scenario layout contract candidate
Status: proposal for peer coordination; no public spelling, version, migration or gesture policy is adopted here. Scope: Scenario only; two independent saved views, lifeline x and direct message send y. This document elaborates [the selected storage direction](scenario-layout-design.md). It is not a replacement for that direction or the user specification. No core implementation or runtime verification accompanies this candidate.
## Existing facts and scope
The working baseline uses schema/source 7. Diagram already owns a representation UUID/alias and architecture placements/routes; StateMachine is an owner-bound projection precedent. Current Scenario rendering derives geometry from a semantic owner without saved lane positions. A Scenario occurrence is independently identified from its represented Exchange; a Lifeline is independently identified from its represented performer. Existing OccurrenceEndpoint contains explicit reference-occurrence IDs and an occurrence ID.
The first slice edits occurrences belonging to the chosen Scenario's own sequence tree, including its nested Fragment/Branch sequences. Here **direct** means no traversal through ScenarioUse or SequenceReference: it does not mean only top-level steps. Expanded called-scenario appearances, process topology, action/state/ref-box placement, independent receive positioning, routes, labels and resize remain outside this payload's first implementation. Their omission is not completion of #20.
## Candidate Rust shape
Names below are proposed. Preserve existing architecture JSON/source fields and defaults; do not migrate them into a new universal payload.
~~~rust
// Additional ProjectionKind variant; existing variants unchanged.
Scenario { scenario: Id }
// Additional Diagram field.
#[serde(default, skip_serializing_if = "Option::is_none")]
layout: Option<DiagramLayout>,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
enum DiagramLayout {
Scenario(ScenarioLayout),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct ScenarioLayout {
#[serde(default)]
lanes: Vec<ScenarioLanePosition>,
#[serde(default)]
messages: Vec<ScenarioMessagePosition>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ScenarioLanePosition {
lifeline: Id,
x: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ScenarioMessagePosition {
address: OccurrenceEndpoint,
send_y: f32,
}
~~~
There are no new row UUIDs, aliases or UUID-valued map keys. Lane row identity is its lifeline reference; message row identity is the complete address. The source declaration identity remains the Diagram UUID. Collections preserve authored order, but their order has no geometric or semantic ordering meaning.
A Scenario projection requires an existing Scenario in the Diagram's layer. It accepts layout absent/none (fully automatic), or the Scenario payload. A present empty payload is valid and distinguishable in the stored model from absence; editing must not gratuitously normalize one to the other. Other projections reject a nonempty Option regardless of whether its contained lists are empty. Scenario diagrams require empty architecture nodes and routes. Existing view_options remain governed by their existing contract; this slice neither invents scenario filter semantics nor silently applies architecture options.
Only one payload field is introduced. Future geometry should extend an appropriate typed payload, not add unrelated optional lane/message/process bags to Diagram. Existing source 7 readers must not accept the new projection or new layout field, including explicit none; the eventual version gate is a coordinated change, not an implication of serde defaulting.
## Exact address and value admission
1. A lane reference must name a Lifeline directly owned by the projected Scenario. Two Lifelines representing the same performer are distinct valid keys.
2. A message address must have references: [] and name an Occurrence whose body is Exchange. Reject any nonempty references list with an explicit unsupported-address diagnostic. Do not resolve it to its terminal occurrence or truncate it. Reusing OccurrenceEndpoint reserves the identity vocabulary, not expanded-reference support.
3. Membership follows the Scenario's actual steps and recursively its own fragment operands/branch steps, consistent with valid parent/step membership. Do not use arbitrary transitive element descendants, represented Exchange identity, or name matching. An unrelated occurrence is invalid even when it represents the same exchange. A Fragment, Action, State, ScenarioUse or SequenceReference occurrence is not a message-position key.
4. Missing sender/receiver/exchange references that are already admitted as unfinished semantic content do not themselves invalidate a correctly owned message-position row. Its authored intent may remain latent until an arrow can be rendered. The renderer must not fabricate endpoints; direct manipulation can begin only on actual geometry. Deleting the message occurrence is different from temporarily removing its endpoint assignment.
5. Reject duplicate lifeline IDs and duplicate complete message addresses after alias resolution, even when values are equal. The same address in two different Diagrams is valid. Unknown fields, missing required lifeline/address/send_y/x, malformed UUIDs and invalid nested records reject the entire candidate.
6. x and send_y are finite binary32 values with absolute value at most existing MAX_COORDINATE (1,000,000 at this baseline). Negative values and signed zero follow the same numeric admission; there is no new positive-only minimum or automatic clamp. Structural admission does not promise a feasible rendered chronology. The separately specified projection must report unsupported geometry and preserve authored intent rather than silently edit semantics or coordinates. Whether the first release refuses such an edit before commit is a separate projection/gesture integration decision.
7. Count one present payload and every lane/message entry against ParseLimits.max_records; these introduce no semantic graph objects. Reference paths are empty in this slice. Enforce shared byte/depth limits before generic allocation, then collection budgets before building membership or duplicate indexes. Validate with bounded traversal/visited identity sets and indexed membership, not one full scene traversal per entry. This counting proposal must be reconciled with #7's checked codec once; it is not a second admission system.
send_y always means the actual message's send endpoint in diagram coordinates. Normal and self messages preserve their renderer-defined send-to-receive offset for whole-message placement. In particular, a self Create/Delete currently stores a receive-oriented row: converting send_y to that row is the renderer bridge's responsibility. Store neither that internal row convention nor activation/capsule coordinates. This contract does not reorder steps, flatten parallel/alternative operands, or prescribe how a user's drag crosses another event.
## JSON and source examples
The following JSON is a complete **candidate Diagram value**, assuming the referenced Scenario/lifeline/message exist with compatible ownership and layer. Version headers belong to the containing project and are deliberately not assigned here.
~~~json
{
"id": "10000000-0000-4000-8000-000000000001",
"alias": "nominal_view",
"name": "Nominal spacing",
"description": "",
"layer": "Oa",
"projection": { "Scenario": { "scenario": "20000000-0000-4000-8000-000000000001" } },
"nodes": {},
"routes": [],
"layout": { "Scenario": {
"lanes": [ { "lifeline": "30000000-0000-4000-8000-000000000001", "x": 420.0 } ],
"messages": [ {
"address": { "references": [], "occurrence": "40000000-0000-4000-8000-000000000001" },
"send_y": 200.0
} ]
} }
}
~~~
Equivalent proposed declarations in a containing source project whose aliases resolve to those identities:
~~~text
diagram nominal_view "Nominal spacing" id "10000000-0000-4000-8000-000000000001" {
layer Oa;
projection { Scenario { scenario @nominal; }; };
layout { Scenario {
lanes [{ lifeline @controller_lane; x 420; }];
messages [{
address { references []; occurrence @delivery_first; };
send_y 200;
}];
}; };
}
diagram review_view "Review spacing" id "10000000-0000-4000-8000-000000000002" {
layer Oa;
projection { Scenario { scenario @nominal; }; };
layout { Scenario {
lanes [{ lifeline @controller_lane; x 680; }];
messages [{ address { references []; occurrence @delivery_first; }; send_y 240; }];
}; };
}
~~~
These are candidate syntax examples, not claims that the current parser accepts them. Source uses @alias in every reference field; UUID string literals remain JSON references and declaration IDs, not a source-reference shortcut.
Invalid variations: repeat controller_lane in one lanes list; repeat the same message address with another send_y; use the represented performer as lifeline; use an Exchange definition as occurrence; use another Scenario's message; use references [@scenario_use]; attach this payload to Overview; add an architecture node or EdgeRoute to this Scenario view; supply NaN/infinity or x beyond the shared bound. Each refusal preserves the entire accepted model/source/history. Equal coordinates for distinct valid lifelines are not a duplicate-key structural error; any graphical overlap feedback belongs to projection policy.
## Commands, sparse values and no-op behavior
CreateDiagram/EditDiagram remain complete-record operations. Candidate focused command names are:
~~~rust
SetScenarioLanePosition { diagram: Id, position: ScenarioLanePosition }
RemoveScenarioLanePosition { diagram: Id, lifeline: Id }
SetScenarioMessagePosition { diagram: Id, position: ScenarioMessagePosition }
RemoveScenarioMessagePosition { diagram: Id, address: OccurrenceEndpoint }
~~~
A Set validates the Scenario context/address, creates its compatible empty payload if absent, replaces an existing row in place or appends a new row, and retains unrelated rows and order. Setting an existing numerically equal value preserves the original record, including signed-zero representation; do not replace -0 with +0 during an otherwise changing batch. Explicitly setting an absent override is an authored change even if it equals today's automatic coordinate. Remove validates the existing diagram and valid semantic address before removing; removing an absent override is a no-op. Removing the final row retains a present empty payload. Explicit EditDiagram can clear it to None.
A gesture with zero actual motion never synthesizes a Set from projected coordinates. Its captured authored value, representation/document/revision/input identity and actual delta govern preview; stale/cancel/no-motion produces no write and no new Diagram. The first actual edit of a derived view prepares a fresh Diagram identity and initial override in one checked transaction; allocation during preview alone must not become a persisted record. Undo returns to the exact prior model/source absence; Redo restores the same created identity.
All command paths use existing prepare_command/execute_batch and checked source writeback. Validate the final batch; reject atomically on structural/source failure. Desktop history captures the complete compiled source snapshot. REST uses its existing If-Match, dry-run and changed:false contract. No new transaction or layout history is introduced. Only the final model/source pair decides no-op: numeric equality must not justify erasing different record provenance or source text.
## Delete, copy, retarget and identity lifecycle
| Operation | Candidate required result |
| --- | --- |
| Delete a saved Diagram | Remove that representation and its inline layout; retain its Scenario, lifelines, occurrences and other views. Existing diagram-owned artifact cleanup still applies. |
| Delete a Scenario | Include all its Scenario-projection Diagrams in the existing deletion closure. No ownerless saved view survives. |
| Delete a Lifeline | Remove its lane entries from all surviving views; existing semantic endpoint cleanup continues. Do not delete message positions merely because those messages become incomplete. |
| Delete an Occurrence, or delete a fragment/branch containing it | Prune entries for the actual deleted message IDs from all surviving views. Preserve unrelated rows. Deleting and recreating a UUID is not a rename: deleted appearance layout is not inherited by the replacement. |
| Hide/fold, change names, change represented performer/exchange | Retain authored layout keyed to the surviving appearance; visibility is not deletion. |
| Reidentify | Rewrite projection owner, lane and occurrence references (and all address components when supported) using the checked whole-project identity map; retain values, row order and source comment ownership. Preflight collisions and final context before publishing mutations. |
| Reparent within the same Scenario sequence tree | Retain position if the final model/address remains valid. Semantic step/ownership validation still applies; geometry feasibility is separately evaluated. |
| Reparent outside the Scenario, or change Exchange occurrence to a non-message body | Reject while incompatible layout remains; an explicit same-batch removal repairs it. No implicit transfer to another view or definition. |
| Retarget Scenario projection | Ordinary EditDiagram may retain only final-compatible rows. In practice a different owner requires clearing old rows explicitly in the supplied replacement; there is no hidden clearing or coordinate reassignment. Layer must also match. |
| Change Scenario projection to architecture | Explicitly remove the Scenario payload and supply valid architecture content in the same transaction. Reverse conversion explicitly removes incompatible architecture nodes/routes, including their identity/provenance handling. |
| Duplicate only a view | Create a fresh Diagram UUID and unique alias; copy payload/order/values and keep all semantic reference IDs unchanged. Do not duplicate semantic elements or externally attached artifacts implicitly. Source creates a new declaration; original comments remain with the original declaration unless a separate copy-source operation promises more. |
| Copy semantic model plus view | An explicit copy map remaps the owner and every copied appearance reference into the copied closure before final validation. Missing/incompatible mappings reject; this is not the view-only duplicate command. No new semantic-copy implementation is claimed. |
Delete/source synchronization must preserve the difference between command deletion and a malformed source edit: source that simply removes a referenced declaration while leaving stale layout is rejected. A deliberate source edit removing both is accepted atomically. Existing source identity correspondence must carry deletion/reuse and UUID swaps; never infer correspondence solely from matching final UUIDs.
## Source lifecycle and exact integration seams
At this worktree baseline:
- oa-core/model.rs:537 (OccurrenceEndpoint), 1431 (ProjectionKind), 1485 (Diagram): add only the Scenario projection and optional tagged payload; preserve existing architecture fields.
- oa-core/session.rs:24-45, 474-489: reuse checked Create/EditDiagram and add focused commands. Audit projection matches, layout and node relocation: no architecture operation may accidentally accept this new view through a wildcard.
- oa-core/language.rs:410-459 already classifies scenario, lifeline, references and occurrence as explicit references. The proposed address object nests existing reference fields; no new UUID map parser is needed. Diagram parser defaults/version gating at1817-1833 and writer at704-724 must cover layout, preserve legacy output when None, and reject the feature under old headers.
- oa-core/source_project.rs:1486 item_key currently recognizes a limited set of nested row reference keys. Extend with schema-aware lane key lifeline and message key full address, resolved through aliases and the original-to-candidate identity map. Do not key by ordinal, x/y, terminal occurrence alone or serialized text. A coordinate edit must preserve unrelated row comments, order, includes and declaration file ownership. It must not turn a source UUID rename into row deletion/reinsertion. update_bundle recompiles and checks the exact requested Project at1017-1025; retain that oracle.
- oa-core/language.rs:59-101 record limits; persistence.rs header admission/migration; validation.rs diagram checks; references.rs remap/deletion closure: integrate all new references and bounded records together.
- oa-desktop/document.rs:315-327 and366-410: unchanged checked source commit/Undo/Redo boundary. app navigation, behavior camera/view state and diagram_usage::Surface must carry saved Diagram identity separately from the semantic Scenario owner; two saved views must not alias one owner-scoped state.
- docs/language.rst Diagram/route lifecycle and docs/api.rst transaction/OpenAPI contract need coordinated normative updates only after adoption. Server may reuse generic commands, but tests must verify serialization, conditional admission, dry-run and exact no-op rather than assume automatic parity.
The future source updater should identify lane/message rows contextually inside this payload, not globally declare every arbitrary record containing a lifeline or occurrence field to be the same row type. Unknown future payload fields remain errors. Reset uses explicit removal, not magical null coordinates.
## #7 coordination and open decisions
Peer issue7 candidate847b1caa7e75bb82f6ce5193c45bcfc45973ad1a already proposes schema8 (model.rs:9), Project.definitions (1661), checked remap preflight (references.rs:214-217), and unfinished analysis source authoring (language.rs:1652-1653). Its remap function returns Result and preflights analysis before mutation. The scenario payload must join that fallible preflight, including duplicate/context validation, rather than restore an infallible signature or add a separately committing remap pass.
Coordinate one version gate/migration, generic Value admission, aggregate record/depth accounting, source nested-row identity/origin handling, reference walking and delete closure. Exact overlap files are model.rs, language.rs, source_project.rs, persistence.rs, references.rs, validation.rs and session.rs, plus their normative docs/API schema. Neither version8 nor these candidate field names are reserved by this document.
Open integration decisions: final public names and version; placement of new source row-key logic alongside peer semantic keys; migration representation of layout absence without rewriting old architecture source; the geometry feasibility contract and selected release gesture; how existing view_options are presented for Scenario views. No option here grants a new movement policy. Later expanded-reference support requires a qualified resolver, render/edit capability, path lifecycle and separate acceptance; accepting its paths now would create unsupported persisted intent.
## Required first-slice evaluators
Before claiming the contract implemented: parse/JSON/source round trips with two distinct views; same performer/exchange repeated under distinct lifeline/occurrence IDs; nested direct message admission and expanded-ref refusal; duplicate aliases resolving to identical address refusal; source comment/include preservation under coordinate edits and identity swaps; deletion/reuse, retarget/reparent repair and view-only duplication; negative/boundary coordinates without silent mutation; zero-motion signed-zero and latent projected values; first edit one-history-entry/Undo-to-absence/Redo-same-ID; stale REST and unchanged dry-run; exact send/receive and typed lifetime/activation geometry. Check original source bytes, accepted revision, undo/redo availability and other-view values on every rejected/no-op path. Pure serialization checks cannot establish gesture or native usability.
+100
View File
@@ -0,0 +1,100 @@
# Persisted scenario editing: implementation direction
Status: selected storage direction after independent source comparison and critique; implementation and acceptance remain open. This note does not adopt a new model/source version or public field spelling. Shared schema, source and reference changes are coordinated in [issue 7 comment 756](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/7#issuecomment-756).
The user requires ordinary direct scenario editing, including spreading lifelines, moving arrows and resizing diagram content, with graphical/text/API parity. [Issue 20](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/20) is the live outcome record; `PROJECT_GOAL.md` retains the full application requirement. Lane and whole-message spacing are the first persisted increment, not completion of that outcome. Process topology, frame/box resizing and route editing remain required work with their own geometry contracts.
## Evidence and choice
Use the existing **Diagram identity namespace**, adding an owner-bound Scenario projection and a typed scenario-layout payload. Preserve current architecture serialization initially. Do not convert every existing diagram to a new generic representation merely to add this capability.
An owner-keyed presentation map is smaller only if there is exactly one arrangement per semantic scenario. That product restriction is not established. Adding view IDs, aliases, naming, duplication, source declarations, deletion and history to that alternative would recreate a second Diagram subsystem. Existing Diagram IDs and the owner-bound StateMachine projection supply useful precedents, while checked Diagram commands and source transactions already exist.
This choice does not make integration automatic. Current behavior navigation, camera scopes and gesture contexts identify semantic owners. Saved views need representation identity throughout those paths. Current non-state saved-diagram dispatch can also fall through to architecture rendering. These are required parts of the first increment, not optional cleanup.
The first projection is **Scenario only**. Process and FunctionalChain currently use topology rendering, while the row-anchor experiment concerns `Scene::sequence`. An unused Process projection with a sequence payload would claim a capability it does not supply. Later process-layout persistence may extend the same Diagram namespace with its appropriate typed payload.
Reviewed source bases:
- [208268c8](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/208268c8aa7f6bc3c2bab6a3c0657358f3f7dc23): prepared navigation/rename union, not yet compiled or accepted together.
- [4b782a71 experiment](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/4b782a71fc011defd39331d3d6d5b73c1506275c/harness/experiments/behavior-anchor-plan-20260912/README.md): complete-anchor bridge, with explicit frame/lifecycle/scope falsifiers. Windows compilation check passed; runtime result was pending when this note was prepared. Use its subsequent evidence record for current results.
- [aadc6396 renderer](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/aadc6396d39ff336d05381d58d545093d3f37a73/crates/oa-desktop/src/behavior.rs): peer typed activation/lifecycle integration. Reported 51 targeted checks do not establish full/native acceptance or this layout integration.
- [847b1caa analysis candidate](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/847b1caa7e75bb82f6ce5193c45bcfc45973ad1a): prospective schema 8, analysis definitions and fallible remapping. Its version and persistence boundary must be reconciled, not overwritten.
## Identity and logical data contract
The following names describe meaning; source/JSON spellings remain subject to the shared schema design.
| Identity/value | Meaning and invariant |
| --- | --- |
| Diagram ID | One saved representation. Two diagrams of one scenario may carry different layouts. It is distinct from the scenario owner ID. |
| Scenario owner ID | The semantic Scenario rendered by that representation; its layer must match the Diagram. |
| Lane identity | The Scenario's Lifeline ID, not its represented performer ID. Two lifelines representing the same performer remain independently addressable. |
| Message appearance | Its occurrence identity plus any reference-occurrence path. Never key by the represented Exchange definition. `OccurrenceEndpoint` is the existing path concept. |
| Lane x override | Authored horizontal center in diagram coordinates. Absence retains automatic placement. |
| Whole-message send-y override | Authored send endpoint height. The receive endpoint and current self-message offset remain derived for a whole-arrow move. A renderer row that denotes receive height must be converted explicitly; row storage conventions must not leak into the public meaning. |
Use typed records with explicit reference-valued fields and reject duplicate addresses. Arbitrary UUID-valued JSON object keys bypass existing source alias/reference handling and are not a suitable shortcut. Direct appearances use an empty reference path. This first increment must not claim expanded-reference editing before its resolver and renderer are implemented and tested; unsupported addresses need explicit admission behavior, not accidental fallback to a definition-level placement.
Coordinate admission reuses the project's finite coordinate bounds. Negative coordinates, large gaps and the resulting viewport extents require actual projection tests; a positive-only fixture does not establish them. Stored values represent authored intent, not a copy of every generated endpoint, label, activation or box bound.
A Scenario projection accepts only its compatible typed layout. Architecture projections reject that payload. Scenario views reject incompatible architecture node/route payloads. Every projection dispatch, selector and usage query must classify the projection explicitly; `state_machine().is_none()` is not an architecture classifier.
Structural validity is separate from the unsettled vertical movement policy. This note does not authorize semantic chronology contradictions, automatic reparenting, unexplained clamping or a new blanket spacing rule.
## Projection and editing lifecycle
1. Opening an old Scenario retains its derived unsaved view. It creates neither a Diagram nor a history entry and does not dirty source.
2. An explicit create/save-view action, or the first real geometry edit, may create a saved Diagram. Creation plus its initial authored override is one checked transaction. Undo restores the prior absence exactly.
3. Resolve absent positions automatically without persisting them. Preserve sparse authored values through folding, hiding, preview and passive layout. A zero-motion drag, including signed zero, must not materialize positions or erase latent values merely because a constraint displaced their displayed geometry.
4. Preview state lives outside the Document. Capture accepted source/document identity, an explicit derived-versus-saved representation identity, semantic owner and the current view-visit/input context when a gesture starts. A derived view has no saved Diagram ID; allocate that ID only inside an accepted creation/edit transaction. Switching between two views of the same Scenario, or leaving and returning to one without an intermediate frame, must invalidate old gesture/navigation envelopes. Cancellation and zero motion retain the derived view without creating a Diagram.
5. Commit one validated transaction on accepted release. Escape, stale context, invalid input and no-op release preserve model, exact source bytes, revision and redo history. Reuse the Document's checked source writeback boundary; do not maintain a parallel layout history.
6. Source export/import, JSON/REST transactions, native Save/reopen and model Undo/Redo must represent the same layout. Existing transaction no-op/ETag behavior remains applicable. Preview geometry alone is not persistence parity.
The first prototype must exercise two separately arranged diagrams of the same Scenario. This exposes representation/owner confusion early, before substantial UI work depends on it.
## References, transformations and limits
Visit the Scenario owner, lifeline IDs, occurrence IDs and every reference-path component in validation, rename/reidentification, deletion and checked remapping. Integrate with the peer's fallible remapping/preflight contract instead of adding an infallible competing walker.
Duplicating a saved view creates a new Diagram identity and copies authored layout values while retaining its semantic owner, lifeline, occurrence and reference-path identities. Duplicating a representation does not reidentify or duplicate its semantic objects. A separate semantic-model copy operation may remap those references according to its explicit copy map; these operations must not share an ambiguous blanket reidentification rule.
Deleting an owner removes its saved Scenario views. Removing an appearance prunes its authored layout according to an explicit, tested rule while preserving unrelated semantic elements and other views. Retargeting a view or reparenting an appearance must reject or explicitly clear incompatible layout; it must not silently apply coordinates to another appearance. Hiding or folding is not deletion and retains authored data.
Nested source records need stable identities and reference handling so a coordinate edit does not rewrite unrelated comments or records. Enforce duplicate-address and collection bounds before unbounded work. Determine these limits through the same checked persistence boundary as other model data; do not invent a second codec admission policy.
## Movement policy still to evaluate
Current ordinary scenario dragging already performs semantic reorder through a validated `SetSteps` command within the same parent sequence. The semantic model orders whole occurrences, not independently ordered send and receive events. Parallel and alternative operands also cannot be treated as one flattened chronology.
Compare these two user gestures before selecting the release behavior:
- Spacing drag preserves semantic order and shows its feasible boundary; an explicit Reorder action changes steps.
- Continuous drag adds spacing and previews a semantic reorder when crossing an event, extending the existing gesture. Release commits the layout and validated reorder atomically when applicable.
Evaluate ordinary spacing, intentional and accidental crossings, Call/Reply and Create/Delete validity, same-owner view switches, discoverability, cancellation and one-step Undo. Both candidates must preserve operand membership unless the user invokes an explicit structural move. A persistence-home choice does not settle this gesture choice.
## Labels are part of the spacing outcome
Both reviewed renderers shorten each complete message label to 32 graphemes independently of lane spacing. Their 225-unit backplate estimate is not a matching text clip. Existing release1 call/reply names already expose this truncation. The navigation membership helper independently assumes the same shortened label; changing paint alone would make its visibility claims wrong.
The next bounded label candidate measures the same single-line text the canvas paints, resolving one displayed text/prefix/bounds result for paint, backplate, hit testing and visible-field navigation. Wider usable space must reveal a full known label when it fits. Measurement must account for the actual canvas zoom, font, shaping and font-system revision. Layout advances are not proof of pixel-ink bounds for overhanging glyphs.
Resolve the corridor after lifecycle/activation endpoint alignment. Reversed and self messages need explicit corridor choices; a zero sender/receiver gap is not a valid self-message policy. The label helper should accept a corridor rather than hide those choices. Wrapping is a separate row-height/collision decision. Preserve the peer's backplate → activation → arrow/text paint order and precise shoulder-hit behavior.
## Required evidence before adoption
| Check | Observable success |
| --- | --- |
| Legacy open / zero motion | No new Diagram, coordinate entries, source changes, revision or history changes. |
| Two views of one Scenario | Independent layouts and camera/input contexts survive switching, export, REST, reopen and Undo. Delayed input from the first cannot edit the second. |
| Repeated Exchange / performer | Moving one appearance changes only its intended authored layout records; other appearance records and semantic identities remain intact. Dependent rendered geometry follows the separately selected movement policy. |
| Whole-message motion | Exact send/receive geometry, marker and activation alignment, with source semantics preserved for spacing and changed only by an explicit accepted reorder transaction. |
| Lifetime and self-call | Delete clipping and unanswered-call cues stay truthful; self-call execution begins at the actual receive endpoint, not its send row. |
| References / copy / deletion | All qualified identities transform consistently; unrelated layouts, comments and semantics remain intact. |
| Label readability | Full known names and distinguishing suffixes appear at sufficient width; actual font/render checks cover CJK, combining text, reversed/self calls, hits and nested bar continuity. |
| Projection extents | Negative coordinates, wide gaps and label extents remain reachable without changing authored values to fit the viewport. |
| Persistence transaction | One real drag yields one source/model history entry; invalid/stale/no-op paths leave source, disk and ETag unchanged as appropriate. |
| Real workflow | A matching Windows build supports direct selection, drag, cancel, next keyboard edit, navigation, Save and reopen on small and representative large models. |
The old floor and complete-plan probes are evidence about integration seams, not an adoptable free-editing architecture. In a later typed-activation union, update their bar/cue representations and compare the full activation vector; preserve nonempty/count and complete source/history assertions. Independent source review alone does not satisfy any runtime row in this table.