Integrate release recovery, semantic review, coverage and Windows packaging

This commit is contained in:
Codex
2026-09-06 16:31:55 -07:00
parent c091f80e6e
commit 2b2d2bc18b
174 changed files with 27635 additions and 127 deletions
+139 -9
View File
@@ -1,3 +1,4 @@
mod checkpoints;
mod hosted;
use crate::forms::Draft;
use crate::{
@@ -260,6 +261,7 @@ pub struct RecoverySnapshot {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
Home,
Diagram,
Catalog,
Behavior,
@@ -281,6 +283,8 @@ impl std::fmt::Display for Choice {
}
#[derive(Debug, Clone, Copy)]
pub enum Pending {
Recent,
Recover(Id),
HostedOpen,
New,
Demo,
@@ -301,6 +305,10 @@ pub struct FilePrompt {
#[derive(Debug, Clone)]
pub enum Message {
Home(crate::home::Event),
CheckpointTick,
CheckpointWritten(Id, Result<(), String>),
CheckpointLoaded(Result<Arc<crate::recovery::Snapshot>, String>),
Hosted(crate::hosted::Event),
PortAnchor(crate::port_edit::Event),
Connection(crate::connection_edit::Event),
@@ -413,6 +421,10 @@ pub enum Message {
}
pub struct App {
pub home: crate::home::State,
pub recovery: crate::recovery::State,
recent_target: Option<PathBuf>,
checkpoint_close: Option<window::Id>,
pub hosted: crate::hosted::State,
pub connection: Option<crate::connection_edit::Session>,
pub model_menu: Option<crate::model_menu::Opened>,
@@ -424,6 +436,8 @@ pub struct App {
pub exact_strings: bool,
pub document: Document,
pub page: Page,
/// Home resumes this page; explicit Architecture navigation remains separate.
last_workspace_page: Page,
pub guide: crate::guide::State,
pub behavior: crate::behavior::State,
pub data_state: crate::data_view::State,
@@ -473,7 +487,11 @@ pub struct App {
impl App {
pub fn new() -> Self {
let mut app = Self::with_project(Project::demo());
let mut app = Self::with_project(Project::empty());
app.home = crate::home::State::load_default();
app.recovery = crate::recovery::State::load_default();
app.page = Page::Home;
app.status = "Create a model, open a project or explore the example.".into();
app.fit();
app
}
@@ -502,7 +520,11 @@ impl App {
let draft = Draft::from_project(&project, selected);
let explorer_index = crate::explorer::Index::new(&project, layer);
let explorer_collapsed = project.elements.values().filter_map(|e| e.parent).collect();
Self {
let mut app = Self {
home: Default::default(),
recovery: Default::default(),
recent_target: None,
checkpoint_close: None,
connection: None,
model_menu: None,
source_tree: Default::default(),
@@ -513,6 +535,7 @@ impl App {
exact_strings: false,
document,
page: Page::Diagram,
last_workspace_page: Page::Diagram,
guide: crate::guide::State::default(),
behavior: Default::default(),
data_state: Default::default(),
@@ -562,7 +585,10 @@ impl App {
explorer_group: None,
explorer_page: 0,
workflow: None,
}
};
app.route_state_diagram_workspace();
app.last_workspace_page = app.page;
app
}
pub fn project(&self) -> &Project {
self.document.project()
@@ -586,6 +612,7 @@ impl App {
}
pub fn dirty(&self) -> bool {
self.document.dirty()
|| self.workflow.is_some()
|| self.draft_dirty()
|| self.source_dirty()
|| self.behavior.has_pending()
@@ -844,6 +871,9 @@ impl App {
}
}
fn reset(&mut self, project: Project, path: Option<PathBuf>) {
self.retire_checkpoint();
let home = std::mem::take(&mut self.home);
let recovery = std::mem::take(&mut self.recovery);
let window_size = self.window_size;
let canvas_size = self.canvas_size;
let source_version = self.source_version + 1;
@@ -852,6 +882,8 @@ impl App {
let mut hosted = std::mem::take(&mut self.hosted);
hosted.leave_document();
*self = Self::with_project(project);
self.home = home;
self.recovery = recovery;
self.hosted = hosted;
self.window_size = window_size;
self.canvas_size = canvas_size;
@@ -923,6 +955,7 @@ impl App {
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch([
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
iced::event::listen_with(|event, status, _| {
@@ -992,6 +1025,9 @@ impl App {
&& !matches!(
message,
Message::Menu(_)
| Message::CheckpointWritten(..)
| Message::CheckpointLoaded(..)
| Message::CheckpointTick
| Message::WindowResized(_)
| Message::CanvasResized(_)
| Message::Lint(_)
@@ -1030,7 +1066,9 @@ impl App {
if self.busy
&& !matches!(
message,
Message::Saved(_)
Message::CheckpointWritten(..)
| Message::CheckpointLoaded(..)
| Message::Saved(_)
| Message::Hosted(crate::hosted::Event::Completed(..))
| Message::Loaded(_)
| Message::IncludesLoaded(_, _)
@@ -1073,7 +1111,10 @@ impl App {
}
let modal_event = matches!(
message,
Message::WindowResized(_)
Message::CheckpointWritten(..)
| Message::CheckpointTick
| Message::CheckpointLoaded(..)
| Message::WindowResized(_)
| Message::CanvasResized(_)
| Message::SourceViewport
| Message::Lint(_)
@@ -1145,7 +1186,14 @@ impl App {
let previous_layer = self.layer;
let entering_catalog =
self.page != Page::Catalog && matches!(message, Message::Page(Page::Catalog));
if self.page != Page::Home {
self.last_workspace_page = self.page;
}
let mut task = self.handle(message);
self.route_state_diagram_workspace();
if self.page != Page::Home {
self.last_workspace_page = self.page;
}
if let Some(context) = self.topology_context() {
self.behavior
.topology
@@ -1206,6 +1254,16 @@ impl App {
}
fn handle(&mut self, message: Message) -> Task<Message> {
match message {
Message::Home(event) => return self.home_event(event),
Message::CheckpointTick => return self.checkpoint_tick(),
Message::CheckpointWritten(id, result) => {
self.checkpoint_written(id, result);
if let Some(window) = self.checkpoint_close.take() {
self.retire_checkpoint();
return window::close(window);
}
}
Message::CheckpointLoaded(result) => return self.checkpoint_loaded(result),
Message::Hosted(event) => return self.hosted_event(event),
Message::ScenarioCanvas(event) => {
use crate::behavior::scenario::{self, CanvasEvent};
@@ -1600,6 +1658,27 @@ impl App {
}
Message::Page(page) => {
if page != Page::Text || !self.draft_dirty() || self.apply() {
if page == Page::Diagram
&& self.active_diagram.is_some_and(|id| {
self.project()
.diagrams
.get(&id)
.is_some_and(|d| d.projection.state_machine().is_some())
})
{
// Architecture remains reachable from a state workspace.
// Its picker can still open a saved state view explicitly.
self.active_diagram = self
.project()
.diagrams
.values()
.filter(|d| {
d.layer == self.layer && d.projection.state_machine().is_none()
})
.min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id))
.map(|d| d.id);
self.fit();
}
if page == Page::Behavior && self.page != Page::Behavior {
let previous_state_view = self.state_view_identity();
if let Some(owner) =
@@ -2545,7 +2624,9 @@ impl App {
self.pending_recovery = None;
}
if receipt.snapshot.bundle.entry != self.document.bundle().entry {
if receipt.revision != self.document.revision() {
if receipt.revision != self.document.revision()
|| self.source_dirty()
{
self.status = format!(
"Snapshot saved to {}. Newer edits retain the current source location.",
receipt.path.display()
@@ -2553,8 +2634,9 @@ impl App {
self.continue_after_save = false;
return Task::none();
}
if let Err(error) =
self.document.apply_bundle(receipt.snapshot.bundle.clone())
if let Err(error) = self
.document
.adopt_saved_entry(&receipt.snapshot, receipt.revision)
{
self.status = error.to_string();
self.continue_after_save = false;
@@ -2568,6 +2650,10 @@ impl App {
self.hosted.review = None;
self.hosted.uncertain = None;
self.document.mark_saved(receipt.snapshot);
self.remember_local();
if !self.dirty() {
self.retire_checkpoint();
}
self.prune_closed_files(!self.document.dirty());
self.status = "All source files saved".into();
} else {
@@ -2608,6 +2694,10 @@ impl App {
match result {
Ok(Some(LoadedSnapshot::Legacy(path, decoded, _bytes))) => {
self.reset(decoded.project, None);
let name = self.project().name.clone();
if let Err(error) = self.home.remember(&path, &name) {
self.home.notice = Some(error);
}
self.migration = decoded.migration;
if self.migration.is_some() {
self.status = "Imported earlier schema · review migration details in Model checks before saving".into();
@@ -2905,6 +2995,11 @@ impl App {
fn proceed(&mut self, pending: Pending) -> Task<Message> {
self.pending = None;
match pending {
Pending::Recent => self
.recent_target
.take()
.map_or_else(Task::none, |path| self.load_from(path)),
Pending::Recover(id) => self.load_checkpoint(id),
Pending::HostedOpen => self.open_hosted(),
Pending::New => {
self.reset(Project::empty(), None);
@@ -2915,7 +3010,16 @@ impl App {
Task::none()
}
Pending::Open => self.choose_file(FileOperation::Open),
Pending::Close(id) => window::close(id),
Pending::Close(id) => {
self.retire_checkpoint();
if self.recovery.writing {
self.checkpoint_close = Some(id);
self.busy = true;
Task::none()
} else {
window::close(id)
}
}
}
}
fn choose_file(&mut self, operation: FileOperation) -> Task<Message> {
@@ -2990,6 +3094,25 @@ impl App {
)
}
fn save_to(&mut self, path: PathBuf) -> Task<Message> {
if !path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
{
let result = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| "Choose a portable .arcadia entry file name.".to_owned())
.and_then(|entry| {
self.document
.check_save_entry(entry)
.map_err(|e| e.to_string())
});
if let Err(error) = result {
self.status = error;
self.continue_after_save = false;
return Task::none();
}
}
self.busy = true;
let snapshot = self.document.snapshot();
let document = self.canvas_document;
@@ -3084,6 +3207,9 @@ impl App {
}
self.pending_recovery = pending_recovery;
self.disk_project = Some(disk);
if self.source_workspace.applied {
self.remember_local();
}
}
fn prune_closed_files(&mut self, prune_expectations: bool) {
@@ -3731,6 +3857,10 @@ fn read_snapshot(path: PathBuf) -> Result<LoadedSnapshot, String> {
))
}
#[cfg(test)]
#[path = "app/save_as_tests.rs"]
mod save_as_tests;
#[cfg(test)]
mod source_project_tests {
use super::*;